diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..79cc938 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,82 @@ +# Copyright 2025 StreamNative +# +# Licensed 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. + +name: E2E Tests + +on: + push: + branches: [main, "release/**"] + pull_request: + branches: [main, "release/**"] + +permissions: + contents: read + +jobs: + pulsar-e2e: + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + + - name: Download dependencies + run: go mod download + + - name: Start Pulsar container + run: | + docker run -d --name pulsar-standalone \ + -p 6650:6650 \ + -p 8080:8080 \ + -v ${{ github.workspace }}/scripts/pulsar-e2e-entrypoint.sh:/pulsar-entrypoint.sh \ + --entrypoint /bin/bash \ + apachepulsar/pulsar:3.3.0 \ + /pulsar-entrypoint.sh + + - name: Wait for Pulsar to be ready + run: | + echo "Waiting for Pulsar to be ready..." + READY=0 + for i in {1..60}; do + if curl -f http://localhost:8080/admin/v2/clusters > /dev/null 2>&1; then + READY=1 + echo "Pulsar is ready!" + break + fi + echo "Attempt $i/60: Pulsar not ready yet..." + sleep 2 + done + if [ "$READY" -ne 1 ]; then + echo "Pulsar failed to become ready within the expected time." + exit 1 + fi + + - name: Run E2E tests + run: go test -v -tags=e2e -race ./pkg/mcp/e2e/... + env: + PULSAR_ADMIN_URL: http://localhost:8080 + PULSAR_SERVICE_URL: pulsar://localhost:6650 + + - name: Show Pulsar logs on failure + if: failure() + run: docker logs pulsar-standalone + + - name: Stop Pulsar container + if: always() + run: docker rm -f pulsar-standalone || true diff --git a/Makefile b/Makefile index 6aa2a26..9e917b9 100644 --- a/Makefile +++ b/Makefile @@ -92,3 +92,43 @@ license-check: .PHONY: license-fix license-fix: license-eye header fix + +# E2E Testing targets +.PHONY: test-e2e-pulsar-start +test-e2e-pulsar-start: + docker-compose -f test/docker-compose-pulsar.yml up -d + @echo "Waiting for Pulsar to be ready..." + @for i in 1 2 3 4 5 6 7 8 9 10; do \ + if curl -s http://localhost:8080/admin/v2/clusters > /dev/null 2>&1; then \ + echo "Pulsar is ready!"; \ + exit 0; \ + fi; \ + echo "Waiting for Pulsar... ($$i/10)"; \ + sleep 3; \ + done + @echo "Error: Pulsar failed to become ready"; \ + exit 1 + +.PHONY: test-e2e-pulsar-stop +test-e2e-pulsar-stop: + docker-compose -f test/docker-compose-pulsar.yml down + +.PHONY: test-e2e-pulsar-logs +test-e2e-pulsar-logs: + docker-compose -f test/docker-compose-pulsar.yml logs + +.PHONY: test-e2e +test-e2e: + go test -tags=e2e -v ./pkg/mcp/e2e/... + +.PHONY: test-e2e-race +test-e2e-race: + go test -race -tags=e2e -v ./pkg/mcp/e2e/... + +.PHONY: test-e2e-run +test-e2e-run: test-e2e-pulsar-start + @echo "Running E2E tests..." + @$(MAKE) test-e2e; \ + result=$$?; \ + $(MAKE) test-e2e-pulsar-stop; \ + exit $$result diff --git a/go.mod b/go.mod index 606d990..e78aea3 100644 --- a/go.mod +++ b/go.mod @@ -8,8 +8,9 @@ require ( github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/google/go-cmp v0.7.0 github.com/hamba/avro/v2 v2.28.0 - github.com/mark3labs/mcp-go v0.43.2 + github.com/invopop/jsonschema v0.13.0 github.com/mitchellh/go-homedir v1.1.0 + github.com/modelcontextprotocol/go-sdk v1.2.0 github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.9.1 @@ -22,7 +23,7 @@ require ( github.com/twmb/franz-go/pkg/kadm v1.16.0 github.com/twmb/franz-go/pkg/sr v1.3.0 github.com/twmb/tlscfg v1.2.1 - golang.org/x/oauth2 v0.27.0 + golang.org/x/oauth2 v0.30.0 gopkg.in/yaml.v2 v2.4.0 k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 ) @@ -49,12 +50,11 @@ require ( github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/uuid v1.6.0 // indirect + github.com/google/jsonschema-go v0.3.0 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/invopop/jsonschema v0.13.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/kris-nova/logger v0.0.0-20181127235838-fd0d87064b06 // indirect diff --git a/go.sum b/go.sum index 8426de5..4b53ae9 100644 --- a/go.sum +++ b/go.sum @@ -97,6 +97,8 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q= +github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= @@ -138,8 +140,6 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -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/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= @@ -158,6 +158,8 @@ github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modelcontextprotocol/go-sdk v1.2.0 h1:Y23co09300CEk8iZ/tMxIX1dVmKZkzoSBZOpJwUnc/s= +github.com/modelcontextprotocol/go-sdk v1.2.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10= 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= @@ -286,8 +288,8 @@ golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -309,6 +311,8 @@ golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A= diff --git a/go.work.sum b/go.work.sum index 84e6fa8..4403462 100644 --- a/go.work.sum +++ b/go.work.sum @@ -103,6 +103,8 @@ github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q= +github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian/v3 v3.0.0 h1:pMen7vLs8nvgEYhywH3KDWJIJTeEr2ULsVWHWYHQyBs= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99 h1:Ak8CrdlwwXwAZxzS66vgPt4U8yUZX7JwLvVR58FN5jM= diff --git a/pkg/cmd/mcp/server.go b/pkg/cmd/mcp/server.go index 5de74c8..75dfdb4 100644 --- a/pkg/cmd/mcp/server.go +++ b/pkg/cmd/mcp/server.go @@ -19,7 +19,6 @@ import ( stdlog "log" "os" - "github.com/mark3labs/mcp-go/server" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/streamnative/streamnative-mcp-server/pkg/config" @@ -30,7 +29,6 @@ import ( func newMcpServer(_ context.Context, configOpts *ServerOptions, logrusLogger *logrus.Logger) (*mcp.Server, error) { snConfig := configOpts.Options.LoadConfigOrDie() - var s *server.MCPServer var mcpServer *mcp.Server switch { case snConfig.KeyFile != "": @@ -46,16 +44,17 @@ func newMcpServer(_ context.Context, configOpts *ServerOptions, logrusLogger *lo if err != nil { return nil, errors.Wrap(err, "failed to create StreamNative Cloud session") } - mcpServer = mcp.NewServer("streamnative-mcp-server", "0.0.1", logrusLogger, server.WithInstructions(mcp.GetStreamNativeCloudServerInstructions(userName, snConfig))) + instructions := mcp.GetStreamNativeCloudServerInstructions(userName, snConfig) + mcpServer = mcp.NewServer("streamnative-mcp-server", "0.0.1", logrusLogger, instructions) mcpServer.SNCloudSession = session - s = mcpServer.MCPServer - mcp.RegisterPrompts(s) + // Register SNCloud-specific tools + mcp.RegisterPrompts(mcpServer.Server) // Skip context tools if pulsar instance and cluster are provided via CLI skipContextTools := snConfig.Context.PulsarInstance != "" && snConfig.Context.PulsarCluster != "" - mcp.RegisterContextTools(s, configOpts.Features, skipContextTools) - mcp.StreamNativeAddLogTools(s, configOpts.ReadOnly, configOpts.Features) - mcp.StreamNativeAddResourceTools(s, configOpts.ReadOnly, configOpts.Features) + mcp.RegisterContextTools(mcpServer.Server, configOpts.Features, skipContextTools) + mcp.StreamNativeAddLogTools(mcpServer.Server, configOpts.ReadOnly, configOpts.Features) + mcp.StreamNativeAddResourceTools(mcpServer.Server, configOpts.ReadOnly, configOpts.Features) } case snConfig.ExternalKafka != nil: { @@ -77,14 +76,14 @@ func newMcpServer(_ context.Context, configOpts *ServerOptions, logrusLogger *lo if err != nil { return nil, errors.Wrap(err, "failed to set external Kafka context") } - mcpServer = mcp.NewServer("streamnative-mcp-server", "0.0.1", logrusLogger, server.WithInstructions(mcp.GetExternalKafkaServerInstructions(snConfig.ExternalKafka.BootstrapServers))) + instructions := mcp.GetExternalKafkaServerInstructions(snConfig.ExternalKafka.BootstrapServers) + mcpServer = mcp.NewServer("streamnative-mcp-server", "0.0.1", logrusLogger, instructions) mcpServer.KafkaSession = ksession - s = mcpServer.MCPServer } case snConfig.ExternalPulsar != nil: { - mcpServer = mcp.NewServer("streamnative-mcp-server", "0.0.1", logrusLogger, server.WithInstructions(mcp.GetExternalPulsarServerInstructions(snConfig.ExternalPulsar.WebServiceURL))) - s = mcpServer.MCPServer + instructions := mcp.GetExternalPulsarServerInstructions(snConfig.ExternalPulsar.WebServiceURL) + mcpServer = mcp.NewServer("streamnative-mcp-server", "0.0.1", logrusLogger, instructions) // Only create global PulsarSession if not in multi-session mode // In multi-session mode, each request must provide its own token via Authorization header @@ -114,6 +113,8 @@ func newMcpServer(_ context.Context, configOpts *ServerOptions, logrusLogger *lo } } + // Register all tools + s := mcpServer.Server mcp.PulsarAdminAddBrokersTools(s, configOpts.ReadOnly, configOpts.Features) mcp.PulsarAdminAddBrokerStatsTools(s, configOpts.ReadOnly, configOpts.Features) mcp.PulsarAdminAddClusterTools(s, configOpts.ReadOnly, configOpts.Features) diff --git a/pkg/cmd/mcp/sse.go b/pkg/cmd/mcp/sse.go index 2620f3a..0e0655d 100644 --- a/pkg/cmd/mcp/sse.go +++ b/pkg/cmd/mcp/sse.go @@ -25,12 +25,11 @@ import ( stdlog "log" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/streamnative/streamnative-mcp-server/pkg/common" "github.com/streamnative/streamnative-mcp-server/pkg/mcp" - context2 "github.com/streamnative/streamnative-mcp-server/pkg/mcp" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/session" "github.com/streamnative/streamnative-mcp-server/pkg/pulsar" ) @@ -75,10 +74,7 @@ func runSseServer(configOpts *ServerOptions) error { return fmt.Errorf("failed to create MCP server: %w", err) } - // 4. Set the context - ctx = context2.WithSNCloudSession(ctx, mcpServer.SNCloudSession) - ctx = context2.WithPulsarSession(ctx, mcpServer.PulsarSession) - ctx = context2.WithKafkaSession(ctx, mcpServer.KafkaSession) + // 4. Set SNCloud context if needed if configOpts.Options.KeyFile != "" { if configOpts.Options.PulsarInstance != "" && configOpts.Options.PulsarCluster != "" { err = mcp.SetContext(ctx, configOpts.Options, configOpts.Options.PulsarInstance, configOpts.Options.PulsarCluster) @@ -115,51 +111,27 @@ func runSseServer(configOpts *ServerOptions) error { logger.Info("Multi-session Pulsar mode enabled") } - sseServer := server.NewSSEServer( - mcpServer.MCPServer, - server.WithStaticBasePath(configOpts.HTTPPath), - server.WithSSEContextFunc(func(ctx context.Context, r *http.Request) context.Context { - c := context.WithValue(ctx, common.OptionsKey, configOpts.Options) - c = context2.WithKafkaSession(c, mcpServer.KafkaSession) - c = context2.WithSNCloudSession(c, mcpServer.SNCloudSession) - - // Handle per-user Pulsar sessions - if pulsarSessionManager != nil { - token := session.ExtractBearerToken(r) - // Token is already validated in auth middleware, this should always succeed - if pulsarSession, err := pulsarSessionManager.GetOrCreateSession(ctx, token); err == nil { - c = context2.WithPulsarSession(c, pulsarSession) - if token != "" { - c = session.WithUserTokenHash(c, pulsarSessionManager.HashTokenForLog(token)) - } - } else { - // Should not happen since middleware validates token first - logger.WithError(err).Error("Unexpected auth error after middleware validation") - // Don't set PulsarSession - tool handlers will fail gracefully with "session not found" - } - } else { - c = context2.WithPulsarSession(c, mcpServer.PulsarSession) - } - - return c - }), + // Create go-sdk StreamableHTTPHandler + // The getServer function returns the server instance for each request + streamableHandler := mcpsdk.NewStreamableHTTPHandler( + func(r *http.Request) *mcpsdk.Server { + return mcpServer.Server + }, + &mcpsdk.StreamableHTTPOptions{ + SessionTimeout: 5 * time.Minute, + }, ) - // 4. Expose the full SSE URL to the user - ssePath := sseServer.CompleteSsePath() - msgPath := sseServer.CompleteMessagePath() + // Build the full path + ssePath := configOpts.HTTPPath fmt.Fprintf(os.Stderr, "StreamNative Cloud MCP Server listening on http://%s%s\n", configOpts.HTTPAddr, ssePath) - // 5. Run the HTTP listener in a goroutine - errCh := make(chan error, 1) - var httpServer *http.Server + // Create HTTP server + mux := http.NewServeMux() if pulsarSessionManager != nil { - // Multi-session mode: use custom handlers with auth middleware - mux := http.NewServeMux() - - // Auth middleware wrapper that validates token before processing + // Multi-session mode: wrap with auth middleware authMiddleware := func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token := session.ExtractBearerToken(r) @@ -175,49 +147,52 @@ func runSseServer(configOpts *ServerOptions) error { http.Error(w, `{"error":"authentication failed"}`, http.StatusUnauthorized) return } + + // Inject sessions into request context + // Use server middleware to pass context to handlers next.ServeHTTP(w, r) }) } - // Mount handlers with auth middleware - mux.Handle(ssePath, authMiddleware(sseServer.SSEHandler())) - mux.Handle(msgPath, authMiddleware(sseServer.MessageHandler())) - - // Start custom HTTP server - httpServer = &http.Server{ - Addr: configOpts.HTTPAddr, - Handler: mux, - ReadHeaderTimeout: 10 * time.Second, // Prevent Slowloris attacks - } - go func() { - if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { - errCh <- err + // Add receiving middleware to inject sessions into MCP handler context + mcpServer.Server.AddReceivingMiddleware(func(next mcpsdk.MethodHandler) mcpsdk.MethodHandler { + return func(ctx context.Context, method string, req mcpsdk.Request) (mcpsdk.Result, error) { + // Extract sessions from context (injected by HTTP middleware) + // Note: go-sdk doesn't directly pass HTTP context to MCP handlers + // We use the base sessions for non-multi-session, or per-request sessions + return next(ctx, method, req) } - }() - logger.Info("SSE server started with authentication middleware") + }) + + mux.Handle(ssePath, authMiddleware(streamableHandler)) } else { - // Non-multi-session mode: use default Start() - go func() { - if err := sseServer.Start(configOpts.HTTPAddr); err != nil && !errors.Is(err, http.ErrServerClosed) { - errCh <- err // bubble up real crashes - } - }() + // Non-multi-session mode: direct handler + mux.Handle(ssePath, streamableHandler) + } + + // Start HTTP server + httpServer := &http.Server{ + Addr: configOpts.HTTPAddr, + Handler: mux, + ReadHeaderTimeout: 10 * time.Second, // Prevent Slowloris attacks } - // Give the server a moment to start - time.Sleep(100 * time.Millisecond) + errCh := make(chan error, 1) + go func() { + if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + errCh <- err + } + }() - // 6. Block until Ctrl-C or an internal error + // Wait for shutdown signal select { case <-ctx.Done(): - // user hit Ctrl-C fmt.Fprintln(os.Stderr, "Received shutdown signal, stopping server...") case err := <-errCh: - // HTTP server crashed return fmt.Errorf("sse server error: %w", err) } - // 7. Graceful shutdown + // Graceful shutdown shCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() @@ -227,28 +202,10 @@ func runSseServer(configOpts *ServerOptions) error { } // Shut down the HTTP server - if httpServer != nil { - // Multi-session mode: shut down custom HTTP server - if err := httpServer.Shutdown(shCtx); err != nil { - if !errors.Is(err, http.ErrServerClosed) { - logger.Errorf("Error shutting down HTTP server: %v", err) - } + if err := httpServer.Shutdown(shCtx); err != nil { + if !errors.Is(err, http.ErrServerClosed) { + logger.Errorf("Error shutting down HTTP server: %v", err) } - } else { - // Non-multi-session mode: shut down SSE server - if err := sseServer.Shutdown(shCtx); err != nil { - if !errors.Is(err, http.ErrServerClosed) { - logger.Errorf("Error shutting down SSE server: %v", err) - } - } - } - - // Wait for any remaining operations to complete - select { - case <-shCtx.Done(): - return fmt.Errorf("shutdown timed out") - case <-time.After(100 * time.Millisecond): - // Give a small grace period for cleanup } fmt.Fprintln(os.Stderr, "SSE server stopped gracefully") diff --git a/pkg/cmd/mcp/stdio.go b/pkg/cmd/mcp/stdio.go index b06ecb6..78ad28f 100644 --- a/pkg/cmd/mcp/stdio.go +++ b/pkg/cmd/mcp/stdio.go @@ -17,18 +17,16 @@ package mcp import ( "context" "fmt" - "io" "os" "os/signal" "syscall" stdlog "log" - "github.com/mark3labs/mcp-go/server" "github.com/sirupsen/logrus" "github.com/spf13/cobra" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/streamnative-mcp-server/pkg/common" - "github.com/streamnative/streamnative-mcp-server/pkg/log" ) func NewCmdMcpStdioServer(configOpts *ServerOptions) *cobra.Command { @@ -61,45 +59,23 @@ func runStdioServer(configOpts *ServerOptions) error { // Create a new MCP server ctx = context.WithValue(ctx, common.OptionsKey, configOpts.Options) - stdLogger := stdlog.New(logger.Writer(), "snmcp-server", 0) mcpServer, err := newMcpServer(ctx, configOpts, logger) if err != nil { return fmt.Errorf("failed to create MCP server: %w", err) } - stdioServer := server.NewStdioServer(mcpServer.MCPServer) - stdioServer.SetErrorLogger(stdLogger) + // Note: command logging (LogCommands) is temporarily disabled in go-sdk migration + // The go-sdk StdioTransport doesn't support custom IO wrapping + // This will be re-implemented in Phase 2 using IOTransport - // Start listening for messages - errC := make(chan error, 1) - go func() { - in, out := io.Reader(os.Stdin), io.Writer(os.Stdout) + // Start the server using go-sdk's StdioTransport + fmt.Fprintf(os.Stderr, "StreamNative Cloud MCP Server running on stdio\n") - if configOpts.LogCommands { - // If command logging is enabled, wrap the IO with a logger - loggedIO := log.NewIOLogger(in, out, logger) - in, out = loggedIO, loggedIO - } - - errC <- stdioServer.Listen(ctx, in, out) - }() - - _, _ = fmt.Fprintf(os.Stderr, "StreamNative Cloud MCP Server running on stdio\n") - - // Wait for shutdown signal - select { - case <-ctx.Done(): - fmt.Fprintf(os.Stderr, "shutting down server...\n") + if err := mcpServer.Server.Run(ctx, &mcpsdk.StdioTransport{}); err != nil { if logger != nil { - logger.Info("Shutting down server...") - } - case err := <-errC: - if err != nil { - if logger != nil { - logger.Errorf("Error running server: %v", err) - } - return fmt.Errorf("error running server: %w", err) + logger.Errorf("Error running server: %v", err) } + return fmt.Errorf("error running server: %w", err) } return nil diff --git a/pkg/mcp/builders/base.go b/pkg/mcp/builders/base.go index bab722a..1ed88c7 100644 --- a/pkg/mcp/builders/base.go +++ b/pkg/mcp/builders/base.go @@ -19,10 +19,16 @@ import ( "fmt" "slices" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" ) +// ServerTool represents a tool with its handler. +// This provides compatibility with the existing builder pattern. +type ServerTool struct { + Tool *mcpsdk.Tool + Handler mcpsdk.ToolHandler +} + // FeatureChecker defines the interface for checking feature requirements // It provides methods to determine if required features are available type FeatureChecker interface { @@ -40,7 +46,7 @@ type ToolBuilder interface { GetRequiredFeatures() []string // BuildTools builds and returns a list of server tools - BuildTools(ctx context.Context, config ToolBuildConfig) ([]server.ServerTool, error) + BuildTools(ctx context.Context, config ToolBuildConfig) ([]ServerTool, error) // Validate validates the builder configuration Validate(config ToolBuildConfig) error @@ -131,4 +137,4 @@ func (b *BaseToolBuilder) HasAnyRequiredFeature(features []string) bool { // ToolHandlerFunc defines the tool handler function type // It maintains consistency with server.ToolHandlerFunc -type ToolHandlerFunc func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) +type ToolHandlerFunc func(ctx context.Context, request mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) diff --git a/pkg/mcp/builders/kafka/connect.go b/pkg/mcp/builders/kafka/connect.go index 43d1218..8bc6487 100644 --- a/pkg/mcp/builders/kafka/connect.go +++ b/pkg/mcp/builders/kafka/connect.go @@ -17,14 +17,13 @@ package kafka import ( "context" "encoding/json" - "fmt" "strings" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/streamnative-mcp-server/pkg/common" "github.com/streamnative/streamnative-mcp-server/pkg/kafka" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" mcpCtx "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" ) @@ -59,7 +58,7 @@ func NewKafkaConnectToolBuilder() *KafkaConnectToolBuilder { // BuildTools builds the Kafka Connect tool list // This is the core method implementing the ToolBuilder interface -func (b *KafkaConnectToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]server.ServerTool, error) { +func (b *KafkaConnectToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]builders.ServerTool, error) { // Check features - return empty list if no required features are present if !b.HasAnyRequiredFeature(config.Features) { return nil, nil @@ -74,7 +73,7 @@ func (b *KafkaConnectToolBuilder) BuildTools(_ context.Context, config builders. tool := b.buildKafkaConnectTool() handler := b.buildKafkaConnectHandler(config.ReadOnly) - return []server.ServerTool{ + return []builders.ServerTool{ { Tool: tool, Handler: handler, @@ -84,7 +83,7 @@ func (b *KafkaConnectToolBuilder) BuildTools(_ context.Context, config builders. // buildKafkaConnectTool builds the Kafka Connect MCP tool definition // Migrated from the original tool definition logic -func (b *KafkaConnectToolBuilder) buildKafkaConnectTool() mcp.Tool { +func (b *KafkaConnectToolBuilder) buildKafkaConnectTool() *mcpsdk.Tool { resourceDesc := "Resource to operate on. Available resources:\n" + "- kafka-connect-cluster: A single Kafka Connect cluster that manages connectors and tasks.\n" + "- connector: A single Kafka Connect connector instance that moves data between Kafka and external systems.\n" + @@ -169,21 +168,22 @@ func (b *KafkaConnectToolBuilder) buildKafkaConnectTool() mcp.Tool { " operation: \"get\"\n\n" + "This tool requires appropriate Kafka Connect permissions." - return mcp.NewTool("kafka_admin_connect", - mcp.WithDescription(toolDesc), - mcp.WithString("resource", mcp.Required(), - mcp.Description(resourceDesc), + return builders.NewTool("kafka_admin_connect", + builders.WithDescription(toolDesc), + builders.WithString("resource", builders.Required(), + builders.Description(resourceDesc), ), - mcp.WithString("operation", mcp.Required(), - mcp.Description(operationDesc), + builders.WithString("operation", builders.Required(), + builders.Description(operationDesc), ), - mcp.WithString("name", - mcp.Description("The name of the Kafka Connect connector to operate on. "+ + builders.WithString("name", + builders.Description("The name of the Kafka Connect connector to operate on. "+ "Required for 'get', 'create', 'update', 'delete', 'restart', 'pause', and 'resume' operations on the 'connector' resource. "+ "Must be unique within the Kafka Connect cluster. "+ - "Should be descriptive of the connector's purpose, such as 'mysql-inventory-source' or 'elasticsearch-logs-sink'.")), - mcp.WithObject("config", - mcp.Description("The configuration settings for the connector. "+ + "Should be descriptive of the connector's purpose, such as 'mysql-inventory-source' or 'elasticsearch-logs-sink'."), + ), + builders.WithObject("config", + builders.Description("The configuration settings for the connector. "+ "Required for 'create' and 'update' operations on the 'connector' resource. "+ "Must include 'connector.class' which specifies the connector implementation. "+ "Common configurations include:\n"+ @@ -192,21 +192,22 @@ func (b *KafkaConnectToolBuilder) buildKafkaConnectTool() mcp.Tool { "- topics/topic.regex/topic.prefix: Topic specification (varies by connector)\n"+ "- key.converter/value.converter: Data format converters\n"+ "- transforms: Optional transformations to apply to data\n"+ - "Additional fields depend on the specific connector type being used.")), + "Additional fields depend on the specific connector type being used."), + ), ) } // buildKafkaConnectHandler builds the Kafka Connect handler function // Migrated from the original handler logic -func (b *KafkaConnectToolBuilder) buildKafkaConnectHandler(readOnly bool) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *KafkaConnectToolBuilder) buildKafkaConnectHandler(readOnly bool) func(context.Context, *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - resource, err := request.RequireString("resource") + resource, err := adapter.RequireString(request, "resource") if err != nil { return b.handleError("get resource", err), nil } - operation, err := request.RequireString("operation") + operation, err := adapter.RequireString(request, "operation") if err != nil { return b.handleError("get operation", err), nil } @@ -217,13 +218,13 @@ func (b *KafkaConnectToolBuilder) buildKafkaConnectHandler(readOnly bool) func(c // Validate write operations in read-only mode if readOnly && (operation == "create" || operation == "update" || operation == "delete" || operation == "restart" || operation == "pause" || operation == "resume") { - return mcp.NewToolResultError("Write operations are not allowed in read-only mode"), nil + return adapter.NewErrorResult("Write operations are not allowed in read-only mode"), nil } // Get Kafka Connect client session := mcpCtx.GetKafkaSession(ctx) if session == nil { - return mcp.NewToolResultError("Kafka session not found in context"), nil + return adapter.NewErrorResult("Kafka session not found in context"), nil } admin, err := session.GetConnectClient() if err != nil { @@ -237,14 +238,14 @@ func (b *KafkaConnectToolBuilder) buildKafkaConnectHandler(readOnly bool) func(c case "get": return b.handleKafkaConnectClusterGet(ctx, admin, request) default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid operation for resource 'kafka-connect-cluster': %s", operation)), nil + return adapter.NewErrorResult("Invalid operation for resource 'kafka-connect-cluster': %s", operation), nil } case "connectors": switch operation { case "list": return b.handleKafkaConnectorsList(ctx, admin, request) default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid operation for resource 'connectors': %s", operation)), nil + return adapter.NewErrorResult("Invalid operation for resource 'connectors': %s", operation), nil } case "connector": switch operation { @@ -263,17 +264,17 @@ func (b *KafkaConnectToolBuilder) buildKafkaConnectHandler(readOnly bool) func(c case "resume": return b.handleKafkaConnectorResume(ctx, admin, request) default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid operation for resource 'connector': %s", operation)), nil + return adapter.NewErrorResult("Invalid operation for resource 'connector': %s", operation), nil } case "connector-plugins": switch operation { case "list": return b.handleKafkaConnectorPluginsList(ctx, admin, request) default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid operation for resource 'connector-plugins': %s", operation)), nil + return adapter.NewErrorResult("Invalid operation for resource 'connector-plugins': %s", operation), nil } default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid resource: %s. Available resources: kafka-connect-cluster, connectors, connector, connector-plugins", resource)), nil + return adapter.NewErrorResult("Invalid resource: %s. Available resources: kafka-connect-cluster, connectors, connector, connector-plugins", resource), nil } } } @@ -281,23 +282,23 @@ func (b *KafkaConnectToolBuilder) buildKafkaConnectHandler(readOnly bool) func(c // Unified error handling and utility functions // handleError provides unified error handling -func (b *KafkaConnectToolBuilder) handleError(operation string, err error) *mcp.CallToolResult { - return mcp.NewToolResultError(fmt.Sprintf("Failed to %s: %v", operation, err)) +func (b *KafkaConnectToolBuilder) handleError(operation string, err error) *mcpsdk.CallToolResult { + return adapter.NewErrorResult("Failed to %s: %v", operation, err) } // marshalResponse provides unified JSON serialization for responses -func (b *KafkaConnectToolBuilder) marshalResponse(data interface{}) (*mcp.CallToolResult, error) { +func (b *KafkaConnectToolBuilder) marshalResponse(data interface{}) (*mcpsdk.CallToolResult, error) { jsonBytes, err := json.Marshal(data) if err != nil { return b.handleError("marshal response", err), nil } - return mcp.NewToolResultText(string(jsonBytes)), nil + return adapter.NewTextResult(string(jsonBytes)), nil } // Specific operation handler functions - migrated from original implementation // handleKafkaConnectClusterGet handles retrieving Kafka Connect cluster information -func (b *KafkaConnectToolBuilder) handleKafkaConnectClusterGet(ctx context.Context, admin kafka.Connect, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *KafkaConnectToolBuilder) handleKafkaConnectClusterGet(ctx context.Context, admin kafka.Connect, _ *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { cluster, err := admin.GetInfo(ctx) if err != nil { return b.handleError("get Kafka Connect cluster", err), nil @@ -306,7 +307,7 @@ func (b *KafkaConnectToolBuilder) handleKafkaConnectClusterGet(ctx context.Conte } // handleKafkaConnectorsList handles listing all connectors -func (b *KafkaConnectToolBuilder) handleKafkaConnectorsList(ctx context.Context, admin kafka.Connect, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *KafkaConnectToolBuilder) handleKafkaConnectorsList(ctx context.Context, admin kafka.Connect, _ *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { connectors, err := admin.ListConnectors(ctx) if err != nil { return b.handleError("get Kafka Connect connectors", err), nil @@ -315,8 +316,8 @@ func (b *KafkaConnectToolBuilder) handleKafkaConnectorsList(ctx context.Context, } // handleKafkaConnectorGet handles retrieving specific connector information -func (b *KafkaConnectToolBuilder) handleKafkaConnectorGet(ctx context.Context, admin kafka.Connect, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - name, err := request.RequireString("name") +func (b *KafkaConnectToolBuilder) handleKafkaConnectorGet(ctx context.Context, admin kafka.Connect, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + name, err := adapter.RequireString(request, "name") if err != nil { return b.handleError("get connector name", err), nil } @@ -329,13 +330,18 @@ func (b *KafkaConnectToolBuilder) handleKafkaConnectorGet(ctx context.Context, a } // handleKafkaConnectorCreate handles creating a new connector -func (b *KafkaConnectToolBuilder) handleKafkaConnectorCreate(ctx context.Context, admin kafka.Connect, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - name, err := request.RequireString("name") +func (b *KafkaConnectToolBuilder) handleKafkaConnectorCreate(ctx context.Context, admin kafka.Connect, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + name, err := adapter.RequireString(request, "name") if err != nil { return b.handleError("get connector name", err), nil } - configMap, err := common.RequiredParamObject(request.GetArguments(), "config") + args, err := adapter.GetArgumentsMap(request) + if err != nil { + return b.handleError("get arguments", err), nil + } + + configMap, err := common.RequiredParamObject(args, "config") if err != nil { return b.handleError("get config", err), nil } @@ -351,13 +357,18 @@ func (b *KafkaConnectToolBuilder) handleKafkaConnectorCreate(ctx context.Context } // handleKafkaConnectorUpdate handles updating a connector -func (b *KafkaConnectToolBuilder) handleKafkaConnectorUpdate(ctx context.Context, admin kafka.Connect, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - name, err := request.RequireString("name") +func (b *KafkaConnectToolBuilder) handleKafkaConnectorUpdate(ctx context.Context, admin kafka.Connect, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + name, err := adapter.RequireString(request, "name") if err != nil { return b.handleError("get connector name", err), nil } - configMap, err := common.RequiredParamObject(request.GetArguments(), "config") + args, err := adapter.GetArgumentsMap(request) + if err != nil { + return b.handleError("get arguments", err), nil + } + + configMap, err := common.RequiredParamObject(args, "config") if err != nil { return b.handleError("get config", err), nil } @@ -373,8 +384,8 @@ func (b *KafkaConnectToolBuilder) handleKafkaConnectorUpdate(ctx context.Context } // handleKafkaConnectorDelete handles deleting a connector -func (b *KafkaConnectToolBuilder) handleKafkaConnectorDelete(ctx context.Context, admin kafka.Connect, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - name, err := request.RequireString("name") +func (b *KafkaConnectToolBuilder) handleKafkaConnectorDelete(ctx context.Context, admin kafka.Connect, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + name, err := adapter.RequireString(request, "name") if err != nil { return b.handleError("get connector name", err), nil } @@ -384,12 +395,12 @@ func (b *KafkaConnectToolBuilder) handleKafkaConnectorDelete(ctx context.Context return b.handleError("delete Kafka Connect connector", err), nil } - return mcp.NewToolResultText("Connector deleted successfully"), nil + return adapter.NewTextResult("Connector deleted successfully"), nil } // handleKafkaConnectorRestart handles restarting a connector -func (b *KafkaConnectToolBuilder) handleKafkaConnectorRestart(ctx context.Context, admin kafka.Connect, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - name, err := request.RequireString("name") +func (b *KafkaConnectToolBuilder) handleKafkaConnectorRestart(ctx context.Context, admin kafka.Connect, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + name, err := adapter.RequireString(request, "name") if err != nil { return b.handleError("get connector name", err), nil } @@ -399,12 +410,12 @@ func (b *KafkaConnectToolBuilder) handleKafkaConnectorRestart(ctx context.Contex return b.handleError("restart Kafka Connect connector", err), nil } - return mcp.NewToolResultText("Connector restarted successfully"), nil + return adapter.NewTextResult("Connector restarted successfully"), nil } // handleKafkaConnectorPause handles pausing a connector -func (b *KafkaConnectToolBuilder) handleKafkaConnectorPause(ctx context.Context, admin kafka.Connect, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - name, err := request.RequireString("name") +func (b *KafkaConnectToolBuilder) handleKafkaConnectorPause(ctx context.Context, admin kafka.Connect, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + name, err := adapter.RequireString(request, "name") if err != nil { return b.handleError("get connector name", err), nil } @@ -414,12 +425,12 @@ func (b *KafkaConnectToolBuilder) handleKafkaConnectorPause(ctx context.Context, return b.handleError("pause Kafka Connect connector", err), nil } - return mcp.NewToolResultText("Connector paused successfully"), nil + return adapter.NewTextResult("Connector paused successfully"), nil } // handleKafkaConnectorResume handles resuming a connector -func (b *KafkaConnectToolBuilder) handleKafkaConnectorResume(ctx context.Context, admin kafka.Connect, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - name, err := request.RequireString("name") +func (b *KafkaConnectToolBuilder) handleKafkaConnectorResume(ctx context.Context, admin kafka.Connect, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + name, err := adapter.RequireString(request, "name") if err != nil { return b.handleError("get connector name", err), nil } @@ -429,11 +440,11 @@ func (b *KafkaConnectToolBuilder) handleKafkaConnectorResume(ctx context.Context return b.handleError("resume Kafka Connect connector", err), nil } - return mcp.NewToolResultText("Connector resumed successfully"), nil + return adapter.NewTextResult("Connector resumed successfully"), nil } // handleKafkaConnectorPluginsList handles listing connector plugins -func (b *KafkaConnectToolBuilder) handleKafkaConnectorPluginsList(ctx context.Context, admin kafka.Connect, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *KafkaConnectToolBuilder) handleKafkaConnectorPluginsList(ctx context.Context, admin kafka.Connect, _ *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { plugins, err := admin.ListPlugins(ctx) if err != nil { return b.handleError("get Kafka Connect connector plugins", err), nil diff --git a/pkg/mcp/builders/kafka/consume.go b/pkg/mcp/builders/kafka/consume.go index 9b8fae2..f23b851 100644 --- a/pkg/mcp/builders/kafka/consume.go +++ b/pkg/mcp/builders/kafka/consume.go @@ -17,14 +17,13 @@ package kafka import ( "context" "encoding/json" - "fmt" "time" "github.com/hamba/avro/v2" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/sirupsen/logrus" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" mcpCtx "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" "github.com/twmb/franz-go/pkg/kgo" "github.com/twmb/franz-go/pkg/sr" @@ -61,7 +60,7 @@ func NewKafkaConsumeToolBuilder() *KafkaConsumeToolBuilder { // BuildTools builds the Kafka consume tool list // This is the core method implementing the ToolBuilder interface -func (b *KafkaConsumeToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]server.ServerTool, error) { +func (b *KafkaConsumeToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]builders.ServerTool, error) { // Check features - return empty list if no required features are present if !b.HasAnyRequiredFeature(config.Features) { return nil, nil @@ -83,7 +82,7 @@ func (b *KafkaConsumeToolBuilder) BuildTools(_ context.Context, config builders. tool := b.buildKafkaConsumeTool() handler := b.buildKafkaConsumeHandler() - return []server.ServerTool{ + return []builders.ServerTool{ { Tool: tool, Handler: handler, @@ -93,7 +92,7 @@ func (b *KafkaConsumeToolBuilder) BuildTools(_ context.Context, config builders. // buildKafkaConsumeTool builds the Kafka consume MCP tool definition // Migrated from the original tool definition logic -func (b *KafkaConsumeToolBuilder) buildKafkaConsumeTool() mcp.Tool { +func (b *KafkaConsumeToolBuilder) buildKafkaConsumeTool() *mcpsdk.Tool { toolDesc := "Consume messages from a Kafka topic.\n" + "This tool allows you to read messages from Kafka topics, specifying various consumption parameters.\n\n" + "Kafka Consumer Concepts:\n" + @@ -121,35 +120,35 @@ func (b *KafkaConsumeToolBuilder) buildKafkaConsumeTool() mcp.Tool { " timeout: 30\n\n" + "This tool requires Kafka consumer permissions on the specified topic." - return mcp.NewTool("kafka_client_consume", - mcp.WithDescription(toolDesc), - mcp.WithString("topic", mcp.Required(), - mcp.Description("The name of the Kafka topic to consume messages from. "+ + return builders.NewTool("kafka_client_consume", + builders.WithDescription(toolDesc), + builders.WithString("topic", builders.Required(), + builders.Description("The name of the Kafka topic to consume messages from. "+ "Must be an existing topic that the user has read permissions for. "+ "For partitioned topics, this will consume from all partitions unless a specific partition is specified."), ), - mcp.WithString("group", - mcp.Description("The consumer group ID to use for consumption. "+ + builders.WithString("group", + builders.Description("The consumer group ID to use for consumption. "+ "Optional. If provided, the consumer will join this consumer group and track offsets with Kafka. "+ "If not provided, a random group ID will be generated, and offsets won't be committed back to Kafka. "+ "Using a meaningful group ID is important when you want to resume consumption later or coordinate multiple consumers."), ), - mcp.WithString("offset", - mcp.Description("The offset position to start consuming from. "+ + builders.WithString("offset", + builders.Description("The offset position to start consuming from. "+ "Optional. Must be one of these values:\n"+ "- 'atstart': Begin from the earliest available message in the topic/partition\n"+ "- 'atend': Begin from the next message that arrives after the consumer starts\n"+ "- 'atcommitted': Begin from the last committed offset (only works with specified 'group')\n"+ "Default: 'atstart'"), ), - mcp.WithNumber("max-messages", - mcp.Description("Maximum number of messages to consume in this request. "+ + builders.WithNumber("max-messages", + builders.Description("Maximum number of messages to consume in this request. "+ "Optional. Limits the total number of messages returned, across all partitions if no specific partition is specified. "+ "Higher values retrieve more data but may increase response time and size. "+ "Default: 10"), ), - mcp.WithNumber("timeout", - mcp.Description("Maximum time in seconds to wait for messages. "+ + builders.WithNumber("timeout", + builders.Description("Maximum time in seconds to wait for messages. "+ "Optional. The consumer will wait up to this long to collect the requested number of messages. "+ "If fewer than 'max-messages' are available within this time, the available messages are returned. "+ "Longer timeouts are useful for low-volume topics or when consuming with 'atend'. "+ @@ -160,11 +159,11 @@ func (b *KafkaConsumeToolBuilder) buildKafkaConsumeTool() mcp.Tool { // buildKafkaConsumeHandler builds the Kafka consume handler function // Migrated from the original handler logic -func (b *KafkaConsumeToolBuilder) buildKafkaConsumeHandler() func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *KafkaConsumeToolBuilder) buildKafkaConsumeHandler() mcpsdk.ToolHandler { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { opts := []kgo.Opt{} // Get required parameters - topicName, err := request.RequireString("topic") + topicName, err := adapter.RequireString(request, "topic") if err != nil { return b.handleError("get topic name", err), nil } @@ -176,16 +175,16 @@ func (b *KafkaConsumeToolBuilder) buildKafkaConsumeHandler() func(context.Contex w := b.logger.Writer() opts = append(opts, kgo.WithLogger(kgo.BasicLogger(w, kgo.LogLevelInfo, nil))) } - maxMessages := request.GetFloat("max-messages", 10) + maxMessages := adapter.GetFloat(request, "max-messages", 10) - timeoutSec := request.GetFloat("timeout", 10) + timeoutSec := adapter.GetFloat(request, "timeout", 10) - group := request.GetString("group", "") + group := adapter.GetString(request, "group", "") if group != "" { opts = append(opts, kgo.ConsumerGroup(group)) } - offsetStr := request.GetString("offset", "atstart") + offsetStr := adapter.GetString(request, "offset", "atstart") var offset kgo.Offset switch offsetStr { @@ -325,15 +324,18 @@ func (b *KafkaConsumeToolBuilder) buildKafkaConsumeHandler() func(context.Contex // Unified error handling and utility functions // handleError provides unified error handling -func (b *KafkaConsumeToolBuilder) handleError(operation string, err error) *mcp.CallToolResult { - return mcp.NewToolResultError(fmt.Sprintf("Failed to %s: %v", operation, err)) +func (b *KafkaConsumeToolBuilder) handleError(operation string, err error) *mcpsdk.CallToolResult { + if err != nil { + return adapter.NewErrorResult("Failed to %s: %v", operation, err) + } + return adapter.NewErrorResult("Failed to %s", operation) } // marshalResponse provides unified JSON serialization for responses -func (b *KafkaConsumeToolBuilder) marshalResponse(data interface{}) (*mcp.CallToolResult, error) { +func (b *KafkaConsumeToolBuilder) marshalResponse(data interface{}) (*mcpsdk.CallToolResult, error) { jsonBytes, err := json.Marshal(data) if err != nil { return b.handleError("marshal response", err), nil } - return mcp.NewToolResultText(string(jsonBytes)), nil + return adapter.NewTextResult(string(jsonBytes)), nil } diff --git a/pkg/mcp/builders/kafka/groups.go b/pkg/mcp/builders/kafka/groups.go index ba4c80c..5f79955 100644 --- a/pkg/mcp/builders/kafka/groups.go +++ b/pkg/mcp/builders/kafka/groups.go @@ -17,12 +17,11 @@ package kafka import ( "context" "encoding/json" - "fmt" "strings" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" mcpCtx "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" "github.com/twmb/franz-go/pkg/kadm" ) @@ -55,7 +54,7 @@ func NewKafkaGroupsToolBuilder() *KafkaGroupsToolBuilder { } // BuildTools builds the Kafka Groups tool list -func (b *KafkaGroupsToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]server.ServerTool, error) { +func (b *KafkaGroupsToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]builders.ServerTool, error) { // Check features - return empty list if no required features are present if !b.HasAnyRequiredFeature(config.Features) { return nil, nil @@ -70,7 +69,7 @@ func (b *KafkaGroupsToolBuilder) BuildTools(_ context.Context, config builders.T tool := b.buildKafkaGroupsTool() handler := b.buildKafkaGroupsHandler(config.ReadOnly) - return []server.ServerTool{ + return []builders.ServerTool{ { Tool: tool, Handler: handler, @@ -79,7 +78,7 @@ func (b *KafkaGroupsToolBuilder) BuildTools(_ context.Context, config builders.T } // buildKafkaGroupsTool builds the Kafka Groups MCP tool definition -func (b *KafkaGroupsToolBuilder) buildKafkaGroupsTool() mcp.Tool { +func (b *KafkaGroupsToolBuilder) buildKafkaGroupsTool() *mcpsdk.Tool { resourceDesc := "Resource to operate on. Available resources:\n" + "- group: A single Kafka Consumer Group for operations on individual groups (describe, remove-members, set-offset, delete-offset)\n" + "- groups: Collection of Kafka Consumer Groups for bulk operations (list)" @@ -130,42 +129,47 @@ func (b *KafkaGroupsToolBuilder) buildKafkaGroupsTool() mcp.Tool { " offset: 1000\n\n" + "This tool requires Kafka super-user permissions." - return mcp.NewTool("kafka_admin_groups", - mcp.WithDescription(toolDesc), - mcp.WithString("resource", mcp.Required(), - mcp.Description(resourceDesc), + return builders.NewTool("kafka_admin_groups", + builders.WithDescription(toolDesc), + builders.WithString("resource", builders.Required(), + builders.Description(resourceDesc), ), - mcp.WithString("operation", mcp.Required(), - mcp.Description(operationDesc), + builders.WithString("operation", builders.Required(), + builders.Description(operationDesc), ), - mcp.WithString("group", - mcp.Description("The name of the Kafka Consumer Group to operate on. "+ + builders.WithString("group", + builders.Description("The name of the Kafka Consumer Group to operate on. "+ "Required for the 'describe' and 'remove-members' operations. "+ "Must be an existing consumer group name in the Kafka cluster. "+ - "Consumer Group names are case-sensitive and typically follow a naming convention like 'application-name'.")), - mcp.WithString("members", - mcp.Description("Comma-separated list of consumer instance IDs to remove from the group. "+ + "Consumer Group names are case-sensitive and typically follow a naming convention like 'application-name'."), + ), + builders.WithString("members", + builders.Description("Comma-separated list of consumer instance IDs to remove from the group. "+ "Required for the 'remove-members' operation. "+ - "Consumer instance IDs can be found using the 'describe' operation.")), - mcp.WithString("topic", - mcp.Description("The topic name. Required for 'delete-offset' and 'set-offset' operations.")), - mcp.WithNumber("partition", - mcp.Description("The partition number. Required for 'set-offset' operation.")), - mcp.WithNumber("offset", - mcp.Description("The offset value to set. Required for 'set-offset' operation.")), + "Consumer instance IDs can be found using the 'describe' operation."), + ), + builders.WithString("topic", + builders.Description("The topic name. Required for 'delete-offset' and 'set-offset' operations."), + ), + builders.WithNumber("partition", + builders.Description("The partition number. Required for 'set-offset' operation."), + ), + builders.WithNumber("offset", + builders.Description("The offset value to set. Required for 'set-offset' operation."), + ), ) } // buildKafkaGroupsHandler builds the Kafka Groups handler function -func (b *KafkaGroupsToolBuilder) buildKafkaGroupsHandler(readOnly bool) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *KafkaGroupsToolBuilder) buildKafkaGroupsHandler(readOnly bool) mcpsdk.ToolHandler { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - resource, err := request.RequireString("resource") + resource, err := adapter.RequireString(request, "resource") if err != nil { return b.handleError("get resource", err), nil } - operation, err := request.RequireString("operation") + operation, err := adapter.RequireString(request, "operation") if err != nil { return b.handleError("get operation", err), nil } @@ -176,7 +180,7 @@ func (b *KafkaGroupsToolBuilder) buildKafkaGroupsHandler(readOnly bool) func(con // Validate write operations in read-only mode if readOnly && (operation == "remove-members" || operation == "delete-offset" || operation == "set-offset") { - return mcp.NewToolResultError("Write operations are not allowed in read-only mode"), nil + return adapter.NewErrorResult("Write operations are not allowed in read-only mode"), nil } // Get Kafka admin client @@ -196,7 +200,7 @@ func (b *KafkaGroupsToolBuilder) buildKafkaGroupsHandler(readOnly bool) func(con case "list": return b.handleKafkaGroupsList(ctx, admin, request) default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid operation for resource 'groups': %s", operation)), nil + return adapter.NewErrorResult("Invalid operation for resource 'groups': %s", operation), nil } case "group": switch operation { @@ -211,10 +215,10 @@ func (b *KafkaGroupsToolBuilder) buildKafkaGroupsHandler(readOnly bool) func(con case "set-offset": return b.handleKafkaGroupSetOffset(ctx, admin, request) default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid operation for resource 'group': %s", operation)), nil + return adapter.NewErrorResult("Invalid operation for resource 'group': %s", operation), nil } default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid resource: %s. Available resources: groups, group", resource)), nil + return adapter.NewErrorResult("Invalid resource: %s. Available resources: groups, group", resource), nil } } } @@ -222,23 +226,26 @@ func (b *KafkaGroupsToolBuilder) buildKafkaGroupsHandler(readOnly bool) func(con // Utility functions // handleError provides unified error handling -func (b *KafkaGroupsToolBuilder) handleError(operation string, err error) *mcp.CallToolResult { - return mcp.NewToolResultError(fmt.Sprintf("Failed to %s: %v", operation, err)) +func (b *KafkaGroupsToolBuilder) handleError(operation string, err error) *mcpsdk.CallToolResult { + if err != nil { + return adapter.NewErrorResult("Failed to %s: %v", operation, err) + } + return adapter.NewErrorResult("Failed to %s", operation) } // marshalResponse provides unified JSON serialization for responses -func (b *KafkaGroupsToolBuilder) marshalResponse(data interface{}) (*mcp.CallToolResult, error) { +func (b *KafkaGroupsToolBuilder) marshalResponse(data interface{}) (*mcpsdk.CallToolResult, error) { jsonBytes, err := json.Marshal(data) if err != nil { return b.handleError("marshal response", err), nil } - return mcp.NewToolResultText(string(jsonBytes)), nil + return adapter.NewTextResult(string(jsonBytes)), nil } // Specific operation handler functions // handleKafkaGroupsList handles listing all consumer groups -func (b *KafkaGroupsToolBuilder) handleKafkaGroupsList(ctx context.Context, admin *kadm.Client, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *KafkaGroupsToolBuilder) handleKafkaGroupsList(ctx context.Context, admin *kadm.Client, _ *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { groups, err := admin.ListGroups(ctx) if err != nil { return b.handleError("list Kafka consumer groups", err), nil @@ -247,8 +254,8 @@ func (b *KafkaGroupsToolBuilder) handleKafkaGroupsList(ctx context.Context, admi } // handleKafkaGroupDescribe handles describing a specific consumer group -func (b *KafkaGroupsToolBuilder) handleKafkaGroupDescribe(ctx context.Context, admin *kadm.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - groupName, err := request.RequireString("group") +func (b *KafkaGroupsToolBuilder) handleKafkaGroupDescribe(ctx context.Context, admin *kadm.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + groupName, err := adapter.RequireString(request, "group") if err != nil { return b.handleError("get group name", err), nil } @@ -261,13 +268,13 @@ func (b *KafkaGroupsToolBuilder) handleKafkaGroupDescribe(ctx context.Context, a } // handleKafkaGroupRemoveMembers handles removing members from a consumer group -func (b *KafkaGroupsToolBuilder) handleKafkaGroupRemoveMembers(ctx context.Context, admin *kadm.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - groupName, err := request.RequireString("group") +func (b *KafkaGroupsToolBuilder) handleKafkaGroupRemoveMembers(ctx context.Context, admin *kadm.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + groupName, err := adapter.RequireString(request, "group") if err != nil { return b.handleError("get group name", err), nil } - membersStr, err := request.RequireString("members") + membersStr, err := adapter.RequireString(request, "members") if err != nil { return b.handleError("get members", err), nil } @@ -287,8 +294,8 @@ func (b *KafkaGroupsToolBuilder) handleKafkaGroupRemoveMembers(ctx context.Conte } // handleKafkaGroupOffsets handles getting offsets for a consumer group -func (b *KafkaGroupsToolBuilder) handleKafkaGroupOffsets(ctx context.Context, admin *kadm.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - groupName, err := request.RequireString("group") +func (b *KafkaGroupsToolBuilder) handleKafkaGroupOffsets(ctx context.Context, admin *kadm.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + groupName, err := adapter.RequireString(request, "group") if err != nil { return b.handleError("get group name", err), nil } @@ -301,13 +308,13 @@ func (b *KafkaGroupsToolBuilder) handleKafkaGroupOffsets(ctx context.Context, ad } // handleKafkaGroupDeleteOffset handles deleting a specific offset for a consumer group -func (b *KafkaGroupsToolBuilder) handleKafkaGroupDeleteOffset(ctx context.Context, admin *kadm.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - groupName, err := request.RequireString("group") +func (b *KafkaGroupsToolBuilder) handleKafkaGroupDeleteOffset(ctx context.Context, admin *kadm.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + groupName, err := adapter.RequireString(request, "group") if err != nil { return b.handleError("get group name", err), nil } - topicName, err := request.RequireString("topic") + topicName, err := adapter.RequireString(request, "topic") if err != nil { return b.handleError("get topic name", err), nil } @@ -332,24 +339,24 @@ func (b *KafkaGroupsToolBuilder) handleKafkaGroupDeleteOffset(ctx context.Contex } // handleKafkaGroupSetOffset handles setting a specific offset for a consumer group -func (b *KafkaGroupsToolBuilder) handleKafkaGroupSetOffset(ctx context.Context, admin *kadm.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - groupName, err := request.RequireString("group") +func (b *KafkaGroupsToolBuilder) handleKafkaGroupSetOffset(ctx context.Context, admin *kadm.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + groupName, err := adapter.RequireString(request, "group") if err != nil { return b.handleError("get group name", err), nil } - topicName, err := request.RequireString("topic") + topicName, err := adapter.RequireString(request, "topic") if err != nil { return b.handleError("get topic name", err), nil } - partitionNum, err := request.RequireFloat("partition") + partitionNum, err := adapter.RequireFloat(request, "partition") if err != nil { return b.handleError("get partition number", err), nil } partitionInt := int32(partitionNum) - offsetNum, err := request.RequireFloat("offset") + offsetNum, err := adapter.RequireFloat(request, "offset") if err != nil { return b.handleError("get offset", err), nil } diff --git a/pkg/mcp/builders/kafka/partitions.go b/pkg/mcp/builders/kafka/partitions.go index c189e17..f2af724 100644 --- a/pkg/mcp/builders/kafka/partitions.go +++ b/pkg/mcp/builders/kafka/partitions.go @@ -17,12 +17,11 @@ package kafka import ( "context" "encoding/json" - "fmt" "strings" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" mcpCtx "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" "github.com/twmb/franz-go/pkg/kadm" ) @@ -55,7 +54,7 @@ func NewKafkaPartitionsToolBuilder() *KafkaPartitionsToolBuilder { } // BuildTools builds the Kafka Partitions tool list -func (b *KafkaPartitionsToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]server.ServerTool, error) { +func (b *KafkaPartitionsToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]builders.ServerTool, error) { // Check features - return empty list if no required features are present if !b.HasAnyRequiredFeature(config.Features) { return nil, nil @@ -70,7 +69,7 @@ func (b *KafkaPartitionsToolBuilder) BuildTools(_ context.Context, config builde tool := b.buildKafkaPartitionsTool() handler := b.buildKafkaPartitionsHandler(config.ReadOnly) - return []server.ServerTool{ + return []builders.ServerTool{ { Tool: tool, Handler: handler, @@ -79,7 +78,7 @@ func (b *KafkaPartitionsToolBuilder) BuildTools(_ context.Context, config builde } // buildKafkaPartitionsTool builds the Kafka Partitions MCP tool definition -func (b *KafkaPartitionsToolBuilder) buildKafkaPartitionsTool() mcp.Tool { +func (b *KafkaPartitionsToolBuilder) buildKafkaPartitionsTool() *mcpsdk.Tool { resourceDesc := "Resource to operate on. Available resources:\n" + "- partition: A single Kafka Partition of a Kafka topic. Partitions are the basic unit of parallelism and data distribution in Kafka." @@ -101,29 +100,29 @@ func (b *KafkaPartitionsToolBuilder) buildKafkaPartitionsTool() mcp.Tool { " resource: \"partition\"\n" + " operation: \"update\"\n" + " topic: \"user-events\"\n" + - " partitions: 6\n\n" + + " new-total: 6\n\n" + "2. Scale up partitions for high-throughput topic:\n" + " resource: \"partition\"\n" + " operation: \"update\"\n" + " topic: \"metrics-data\"\n" + - " partitions: 12\n\n" + + " new-total: 12\n\n" + "This tool requires appropriate Kafka permissions for partition management." - return mcp.NewTool("kafka_admin_partitions", - mcp.WithDescription(toolDesc), - mcp.WithString("resource", mcp.Required(), - mcp.Description(resourceDesc), + return builders.NewTool("kafka_admin_partitions", + builders.WithDescription(toolDesc), + builders.WithString("resource", builders.Required(), + builders.Description(resourceDesc), ), - mcp.WithString("operation", mcp.Required(), - mcp.Description(operationDesc), + builders.WithString("operation", builders.Required(), + builders.Description(operationDesc), ), - mcp.WithString("topic", - mcp.Description("The name of the Kafka topic to operate on. "+ + builders.WithString("topic", + builders.Description("The name of the Kafka topic to operate on. "+ "Required for the 'update' operation. "+ "Must be an existing topic name in the Kafka cluster. "+ "The topic must be in a healthy state for partition updates to succeed.")), - mcp.WithNumber("new-total", - mcp.Description("The new total number of partitions for the Kafka topic. "+ + builders.WithNumber("new-total", + builders.Description("The new total number of partitions for the Kafka topic. "+ "Required for the 'update' operation. "+ "Must be greater than the current number of partitions (cannot decrease partitions). "+ "A larger number of partitions can help increase parallelism and throughput, but may also "+ @@ -133,15 +132,15 @@ func (b *KafkaPartitionsToolBuilder) buildKafkaPartitionsTool() mcp.Tool { } // buildKafkaPartitionsHandler builds the Kafka Partitions handler function -func (b *KafkaPartitionsToolBuilder) buildKafkaPartitionsHandler(readOnly bool) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *KafkaPartitionsToolBuilder) buildKafkaPartitionsHandler(readOnly bool) mcpsdk.ToolHandler { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - resource, err := request.RequireString("resource") + resource, err := adapter.RequireString(request, "resource") if err != nil { return b.handleError("get resource", err), nil } - operation, err := request.RequireString("operation") + operation, err := adapter.RequireString(request, "operation") if err != nil { return b.handleError("get operation", err), nil } @@ -152,7 +151,7 @@ func (b *KafkaPartitionsToolBuilder) buildKafkaPartitionsHandler(readOnly bool) // Validate write operations in read-only mode if readOnly && operation == "update" { - return mcp.NewToolResultError("Write operations are not allowed in read-only mode"), nil + return adapter.NewErrorResult("Write operations are not allowed in read-only mode"), nil } // Get Kafka admin client @@ -172,10 +171,10 @@ func (b *KafkaPartitionsToolBuilder) buildKafkaPartitionsHandler(readOnly bool) case "update": return b.handleKafkaPartitionUpdate(ctx, admin, request) default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid operation for resource 'partition': %s", operation)), nil + return adapter.NewErrorResult("Invalid operation for resource 'partition': %s", operation), nil } default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid resource: %s. Available resources: partition", resource)), nil + return adapter.NewErrorResult("Invalid resource: %s. Available resources: partition", resource), nil } } } @@ -183,32 +182,36 @@ func (b *KafkaPartitionsToolBuilder) buildKafkaPartitionsHandler(readOnly bool) // Utility functions // handleError provides unified error handling -func (b *KafkaPartitionsToolBuilder) handleError(operation string, err error) *mcp.CallToolResult { - return mcp.NewToolResultError(fmt.Sprintf("Failed to %s: %v", operation, err)) +func (b *KafkaPartitionsToolBuilder) handleError(operation string, err error) *mcpsdk.CallToolResult { + if err != nil { + return adapter.NewErrorResult("Failed to %s: %v", operation, err) + } + return adapter.NewErrorResult("Failed to %s", operation) } // marshalResponse provides unified JSON serialization for responses -func (b *KafkaPartitionsToolBuilder) marshalResponse(data interface{}) (*mcp.CallToolResult, error) { +func (b *KafkaPartitionsToolBuilder) marshalResponse(data interface{}) (*mcpsdk.CallToolResult, error) { jsonBytes, err := json.Marshal(data) if err != nil { return b.handleError("marshal response", err), nil } - return mcp.NewToolResultText(string(jsonBytes)), nil + return adapter.NewTextResult(string(jsonBytes)), nil } // handleKafkaPartitionUpdate handles updating the number of partitions for a topic -func (b *KafkaPartitionsToolBuilder) handleKafkaPartitionUpdate(ctx context.Context, admin *kadm.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - topicName, err := request.RequireString("topic") +func (b *KafkaPartitionsToolBuilder) handleKafkaPartitionUpdate(ctx context.Context, admin *kadm.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + topicName, err := adapter.RequireString(request, "topic") if err != nil { return b.handleError("get topic name", err), nil } - newTotal, err := request.RequireInt("new-total") + newTotalFloat, err := adapter.RequireFloat(request, "new-total") if err != nil { return b.handleError("get new total", err), nil } + newTotal := int32(newTotalFloat) - response, err := admin.UpdatePartitions(ctx, newTotal, topicName) + response, err := admin.UpdatePartitions(ctx, int(newTotal), topicName) if err != nil { return b.handleError("update Kafka topic partitions", err), nil } diff --git a/pkg/mcp/builders/kafka/produce.go b/pkg/mcp/builders/kafka/produce.go index 0c7596c..50fddb2 100644 --- a/pkg/mcp/builders/kafka/produce.go +++ b/pkg/mcp/builders/kafka/produce.go @@ -17,14 +17,13 @@ package kafka import ( "context" "encoding/json" - "fmt" "strings" "time" "github.com/hamba/avro/v2" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" mcpCtx "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" "github.com/twmb/franz-go/pkg/kgo" "github.com/twmb/franz-go/pkg/sr" @@ -60,7 +59,7 @@ func NewKafkaProduceToolBuilder() *KafkaProduceToolBuilder { // BuildTools builds the Kafka produce tool list // This is the core method implementing the ToolBuilder interface -func (b *KafkaProduceToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]server.ServerTool, error) { +func (b *KafkaProduceToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]builders.ServerTool, error) { // Skip registration if in read-only mode if config.ReadOnly { return nil, nil @@ -80,7 +79,7 @@ func (b *KafkaProduceToolBuilder) BuildTools(_ context.Context, config builders. tool := b.buildKafkaProduceTool() handler := b.buildKafkaProduceHandler() - return []server.ServerTool{ + return []builders.ServerTool{ { Tool: tool, Handler: handler, @@ -90,7 +89,7 @@ func (b *KafkaProduceToolBuilder) BuildTools(_ context.Context, config builders. // buildKafkaProduceTool builds the Kafka produce MCP tool definition // Migrated from the original tool definition logic -func (b *KafkaProduceToolBuilder) buildKafkaProduceTool() mcp.Tool { +func (b *KafkaProduceToolBuilder) buildKafkaProduceTool() *mcpsdk.Tool { toolDesc := "Produce messages to a Kafka topic.\n" + "This tool allows you to send messages to Kafka topics with various options for message creation.\n\n" + "Kafka Producer Concepts:\n" + @@ -120,42 +119,42 @@ func (b *KafkaProduceToolBuilder) buildKafkaProduceTool() mcp.Tool { " partition: 2\n\n" + "This tool requires Kafka producer permissions on the specified topic." - return mcp.NewTool("kafka_client_produce", - mcp.WithDescription(toolDesc), - mcp.WithString("topic", mcp.Required(), - mcp.Description("The name of the Kafka topic to produce messages to. "+ + return builders.NewTool("kafka_client_produce", + builders.WithDescription(toolDesc), + builders.WithString("topic", builders.Required(), + builders.Description("The name of the Kafka topic to produce messages to. "+ "Must be an existing topic that the user has write permissions for."), ), - mcp.WithString("key", - mcp.Description("The key for the message. "+ + builders.WithString("key", + builders.Description("The key for the message. "+ "Optional. Keys are used for partition assignment and maintaining order for related messages. "+ "Messages with the same key will be sent to the same partition."), ), - mcp.WithString("value", - mcp.Required(), - mcp.Description("The value/content of the message to send. "+ + builders.WithString("value", + builders.Required(), + builders.Description("The value/content of the message to send. "+ "This is the actual payload that will be delivered to consumers. It can be a JSON string, and the system will automatically serialize it to the appropriate format based on the schema registry if it is available."), ), - mcp.WithArray("headers", - mcp.Description("Message headers in the format of [\"key=value\"]. "+ + builders.WithArray("headers", + builders.Description("Message headers in the format of [\"key=value\"]. "+ "Optional. Headers allow you to attach metadata to messages without modifying the payload. "+ "They are passed along with the message to consumers."), - mcp.Items(map[string]interface{}{ + builders.Items(map[string]interface{}{ "type": "string", "description": "key value pair in the format of \"key=value\"", }), ), - mcp.WithNumber("partition", - mcp.Description("The specific partition to send the message to. "+ + builders.WithNumber("partition", + builders.Description("The specific partition to send the message to. "+ "Optional. If not specified, Kafka will automatically assign a partition based on the message key (if provided) or round-robin assignment. "+ "Specifying a partition can be useful for testing or when you need guaranteed partition assignment."), ), - mcp.WithArray("messages", - mcp.Description("An array of messages to send in batch. "+ + builders.WithArray("messages", + builders.Description("An array of messages to send in batch. "+ "Optional. Alternative to the single message parameters (key, value, headers, partition). "+ "Each message object can contain 'key', 'value', 'headers', and 'partition' properties. "+ "Batch sending is more efficient for multiple messages."), - mcp.Items(map[string]interface{}{ + builders.Items(map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "key": map[string]interface{}{ @@ -181,8 +180,8 @@ func (b *KafkaProduceToolBuilder) buildKafkaProduceTool() mcp.Tool { "required": []string{"value"}, }), ), - mcp.WithBoolean("sync", - mcp.Description("Whether to wait for server acknowledgment before returning. "+ + builders.WithBoolean("sync", + builders.Description("Whether to wait for server acknowledgment before returning. "+ "Optional. Default is true. When true, ensures the message was successfully written "+ "to the topic before the tool returns a success response."), ), @@ -191,10 +190,10 @@ func (b *KafkaProduceToolBuilder) buildKafkaProduceTool() mcp.Tool { // buildKafkaProduceHandler builds the Kafka produce handler function // Migrated from the original handler logic -func (b *KafkaProduceToolBuilder) buildKafkaProduceHandler() func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *KafkaProduceToolBuilder) buildKafkaProduceHandler() mcpsdk.ToolHandler { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required topic parameter - topicName, err := request.RequireString("topic") + topicName, err := adapter.RequireString(request, "topic") if err != nil { return b.handleError("get topic name", err), nil } @@ -263,14 +262,14 @@ func (b *KafkaProduceToolBuilder) buildKafkaProduceHandler() func(context.Contex } // Single message mode (simplified version) - value, err := request.RequireString("value") + value, err := adapter.RequireString(request, "value") if err != nil { return b.handleError("get value", err), nil } - key := request.GetString("key", "") - headers := request.GetStringSlice("headers", []string{}) - sync := request.GetBool("sync", true) + key := adapter.GetString(request, "key", "") + headers := adapter.GetStringSlice(request, "headers", []string{}) + sync := adapter.GetBool(request, "sync", true) // Prepare record record := &kgo.Record{ @@ -345,15 +344,18 @@ func (b *KafkaProduceToolBuilder) buildKafkaProduceHandler() func(context.Contex // Unified error handling and utility functions // handleError provides unified error handling -func (b *KafkaProduceToolBuilder) handleError(operation string, err error) *mcp.CallToolResult { - return mcp.NewToolResultError(fmt.Sprintf("Failed to %s: %v", operation, err)) +func (b *KafkaProduceToolBuilder) handleError(operation string, err error) *mcpsdk.CallToolResult { + if err != nil { + return adapter.NewErrorResult("Failed to %s: %v", operation, err) + } + return adapter.NewErrorResult("Failed to %s", operation) } // marshalResponse provides unified JSON serialization for responses -func (b *KafkaProduceToolBuilder) marshalResponse(data interface{}) (*mcp.CallToolResult, error) { +func (b *KafkaProduceToolBuilder) marshalResponse(data interface{}) (*mcpsdk.CallToolResult, error) { jsonBytes, err := json.Marshal(data) if err != nil { return b.handleError("marshal response", err), nil } - return mcp.NewToolResultText(string(jsonBytes)), nil + return adapter.NewTextResult(string(jsonBytes)), nil } diff --git a/pkg/mcp/builders/kafka/schema_registry.go b/pkg/mcp/builders/kafka/schema_registry.go index a4a0eae..dec3cef 100644 --- a/pkg/mcp/builders/kafka/schema_registry.go +++ b/pkg/mcp/builders/kafka/schema_registry.go @@ -21,9 +21,9 @@ import ( "strconv" "strings" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" mcpCtx "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" "github.com/twmb/franz-go/pkg/sr" ) @@ -57,7 +57,7 @@ func NewKafkaSchemaRegistryToolBuilder() *KafkaSchemaRegistryToolBuilder { } // BuildTools builds the Kafka Schema Registry tool list -func (b *KafkaSchemaRegistryToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]server.ServerTool, error) { +func (b *KafkaSchemaRegistryToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]builders.ServerTool, error) { // Check features - return empty list if no required features are present if !b.HasAnyRequiredFeature(config.Features) { return nil, nil @@ -72,7 +72,7 @@ func (b *KafkaSchemaRegistryToolBuilder) BuildTools(_ context.Context, config bu tool := b.buildKafkaSchemaRegistryTool() handler := b.buildKafkaSchemaRegistryHandler(config.ReadOnly) - return []server.ServerTool{ + return []builders.ServerTool{ { Tool: tool, Handler: handler, @@ -81,7 +81,7 @@ func (b *KafkaSchemaRegistryToolBuilder) BuildTools(_ context.Context, config bu } // buildKafkaSchemaRegistryTool builds the Kafka Schema Registry MCP tool definition -func (b *KafkaSchemaRegistryToolBuilder) buildKafkaSchemaRegistryTool() mcp.Tool { +func (b *KafkaSchemaRegistryToolBuilder) buildKafkaSchemaRegistryTool() *mcpsdk.Tool { resourceDesc := "Resource to operate on. Available resources:\n" + "- subjects: Collection of all schema subjects in the Schema Registry\n" + "- subject: A specific schema subject (a named schema that can have multiple versions)\n" + @@ -131,46 +131,46 @@ func (b *KafkaSchemaRegistryToolBuilder) buildKafkaSchemaRegistryTool() mcp.Tool " compatibility: \"BACKWARD\"\n\n" + "This tool requires appropriate Schema Registry permissions." - return mcp.NewTool("kafka_admin_sr", - mcp.WithDescription(toolDesc), - mcp.WithString("resource", mcp.Required(), - mcp.Description(resourceDesc), + return builders.NewTool("kafka_admin_sr", + builders.WithDescription(toolDesc), + builders.WithString("resource", builders.Required(), + builders.Description(resourceDesc), ), - mcp.WithString("operation", mcp.Required(), - mcp.Description(operationDesc), + builders.WithString("operation", builders.Required(), + builders.Description(operationDesc), ), - mcp.WithString("subject", - mcp.Description("The name of the schema subject. "+ + builders.WithString("subject", + builders.Description("The name of the schema subject. "+ "Required for operations on 'subject', 'versions', 'version', and subject-specific 'compatibility' resources. "+ "Subject names typically follow the pattern '-key' or '-value'.")), - mcp.WithString("version", - mcp.Description("The version number or 'latest' for the most recent version. "+ + builders.WithString("version", + builders.Description("The version number or 'latest' for the most recent version. "+ "Required for 'version' resource operations.")), - mcp.WithString("compatibility", - mcp.Description("The compatibility level to set. "+ + builders.WithString("compatibility", + builders.Description("The compatibility level to set. "+ "Valid values: BACKWARD, FORWARD, FULL, NONE. "+ "Required for 'set' operation on 'compatibility' resource.")), - mcp.WithString("schemaType", - mcp.Description("The schema format type. "+ + builders.WithString("schemaType", + builders.Description("The schema format type. "+ "Valid values: AVRO, JSON, PROTOBUF. "+ "Required for 'create' operation on 'subject' resource.")), - mcp.WithObject("schema", - mcp.Description("The schema definition as a JSON object. "+ + builders.WithObject("schema", + builders.Description("The schema definition as a JSON object. "+ "Required for 'create' operation on 'subject' resource. "+ "The structure depends on the schema type (AVRO, JSON Schema, or Protocol Buffers).")), ) } // buildKafkaSchemaRegistryHandler builds the Kafka Schema Registry handler function -func (b *KafkaSchemaRegistryToolBuilder) buildKafkaSchemaRegistryHandler(readOnly bool) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *KafkaSchemaRegistryToolBuilder) buildKafkaSchemaRegistryHandler(readOnly bool) mcpsdk.ToolHandler { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - resource, err := request.RequireString("resource") + resource, err := adapter.RequireString(request, "resource") if err != nil { return b.handleError("get resource", err), nil } - operation, err := request.RequireString("operation") + operation, err := adapter.RequireString(request, "operation") if err != nil { return b.handleError("get operation", err), nil } @@ -181,7 +181,7 @@ func (b *KafkaSchemaRegistryToolBuilder) buildKafkaSchemaRegistryHandler(readOnl // Validate write operations in read-only mode if readOnly && (operation == "create" || operation == "delete" || operation == "set") { - return mcp.NewToolResultError("Write operations are not allowed in read-only mode"), nil + return adapter.NewErrorResult("Write operations are not allowed in read-only mode"), nil } // Get Schema Registry client @@ -201,7 +201,7 @@ func (b *KafkaSchemaRegistryToolBuilder) buildKafkaSchemaRegistryHandler(readOnl case "list": return b.handleSchemaSubjectsList(ctx, client, request) default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid operation for resource 'subjects': %s", operation)), nil + return adapter.NewErrorResult("Invalid operation for resource 'subjects': %s", operation), nil } case "subject": switch operation { @@ -212,14 +212,14 @@ func (b *KafkaSchemaRegistryToolBuilder) buildKafkaSchemaRegistryHandler(readOnl case "delete": return b.handleSchemaSubjectDelete(ctx, client, request) default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid operation for resource 'subject': %s", operation)), nil + return adapter.NewErrorResult("Invalid operation for resource 'subject': %s", operation), nil } case "versions": switch operation { case "list": return b.handleSchemaVersionsList(ctx, client, request) default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid operation for resource 'versions': %s", operation)), nil + return adapter.NewErrorResult("Invalid operation for resource 'versions': %s", operation), nil } case "version": switch operation { @@ -228,7 +228,7 @@ func (b *KafkaSchemaRegistryToolBuilder) buildKafkaSchemaRegistryHandler(readOnl case "delete": return b.handleSchemaVersionDelete(ctx, client, request) default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid operation for resource 'version': %s", operation)), nil + return adapter.NewErrorResult("Invalid operation for resource 'version': %s", operation), nil } case "compatibility": switch operation { @@ -237,17 +237,17 @@ func (b *KafkaSchemaRegistryToolBuilder) buildKafkaSchemaRegistryHandler(readOnl case "set": return b.handleSchemaCompatibilitySet(ctx, client, request) default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid operation for resource 'compatibility': %s", operation)), nil + return adapter.NewErrorResult("Invalid operation for resource 'compatibility': %s", operation), nil } case "types": switch operation { case "list": return b.handleSchemaTypesList(ctx, client, request) default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid operation for resource 'types': %s", operation)), nil + return adapter.NewErrorResult("Invalid operation for resource 'types': %s", operation), nil } default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid resource: %s. Available resources: subjects, subject, versions, version, compatibility, types", resource)), nil + return adapter.NewErrorResult("Invalid resource: %s. Available resources: subjects, subject, versions, version, compatibility, types", resource), nil } } } @@ -255,23 +255,26 @@ func (b *KafkaSchemaRegistryToolBuilder) buildKafkaSchemaRegistryHandler(readOnl // Utility functions // handleError provides unified error handling -func (b *KafkaSchemaRegistryToolBuilder) handleError(operation string, err error) *mcp.CallToolResult { - return mcp.NewToolResultError(fmt.Sprintf("Failed to %s: %v", operation, err)) +func (b *KafkaSchemaRegistryToolBuilder) handleError(operation string, err error) *mcpsdk.CallToolResult { + if err != nil { + return adapter.NewErrorResult("Failed to %s: %v", operation, err) + } + return adapter.NewErrorResult("Failed to %s", operation) } // marshalResponse provides unified JSON serialization for responses -func (b *KafkaSchemaRegistryToolBuilder) marshalResponse(data interface{}) (*mcp.CallToolResult, error) { +func (b *KafkaSchemaRegistryToolBuilder) marshalResponse(data interface{}) (*mcpsdk.CallToolResult, error) { jsonBytes, err := json.Marshal(data) if err != nil { return b.handleError("marshal response", err), nil } - return mcp.NewToolResultText(string(jsonBytes)), nil + return adapter.NewTextResult(string(jsonBytes)), nil } // Specific operation handler functions // handleSchemaSubjectsList handles listing all schema subjects -func (b *KafkaSchemaRegistryToolBuilder) handleSchemaSubjectsList(ctx context.Context, client *sr.Client, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *KafkaSchemaRegistryToolBuilder) handleSchemaSubjectsList(ctx context.Context, client *sr.Client, _ *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { subjects, err := client.Subjects(ctx) if err != nil { return b.handleError("list schema subjects", err), nil @@ -280,8 +283,8 @@ func (b *KafkaSchemaRegistryToolBuilder) handleSchemaSubjectsList(ctx context.Co } // handleSchemaSubjectGet handles getting the latest schema for a subject -func (b *KafkaSchemaRegistryToolBuilder) handleSchemaSubjectGet(ctx context.Context, client *sr.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - subject, err := request.RequireString("subject") +func (b *KafkaSchemaRegistryToolBuilder) handleSchemaSubjectGet(ctx context.Context, client *sr.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + subject, err := adapter.RequireString(request, "subject") if err != nil { return b.handleError("get subject name", err), nil } @@ -294,18 +297,18 @@ func (b *KafkaSchemaRegistryToolBuilder) handleSchemaSubjectGet(ctx context.Cont } // handleSchemaSubjectCreate handles registering a new schema for a subject -func (b *KafkaSchemaRegistryToolBuilder) handleSchemaSubjectCreate(ctx context.Context, client *sr.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - subject, err := request.RequireString("subject") +func (b *KafkaSchemaRegistryToolBuilder) handleSchemaSubjectCreate(ctx context.Context, client *sr.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + subject, err := adapter.RequireString(request, "subject") if err != nil { return b.handleError("get subject name", err), nil } - schemaTypeStr, err := request.RequireString("schemaType") + schemaTypeStr, err := adapter.RequireString(request, "schemaType") if err != nil { return b.handleError("get schema type", err), nil } - schema, err := request.RequireString("schema") + schema, err := adapter.RequireString(request, "schema") if err != nil { return b.handleError("get schema object", err), nil } @@ -331,8 +334,8 @@ func (b *KafkaSchemaRegistryToolBuilder) handleSchemaSubjectCreate(ctx context.C } // handleSchemaSubjectDelete handles deleting a schema subject -func (b *KafkaSchemaRegistryToolBuilder) handleSchemaSubjectDelete(ctx context.Context, client *sr.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - subject, err := request.RequireString("subject") +func (b *KafkaSchemaRegistryToolBuilder) handleSchemaSubjectDelete(ctx context.Context, client *sr.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + subject, err := adapter.RequireString(request, "subject") if err != nil { return b.handleError("get subject name", err), nil } @@ -346,8 +349,8 @@ func (b *KafkaSchemaRegistryToolBuilder) handleSchemaSubjectDelete(ctx context.C } // handleSchemaVersionsList handles listing all versions for a subject -func (b *KafkaSchemaRegistryToolBuilder) handleSchemaVersionsList(ctx context.Context, client *sr.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - subject, err := request.RequireString("subject") +func (b *KafkaSchemaRegistryToolBuilder) handleSchemaVersionsList(ctx context.Context, client *sr.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + subject, err := adapter.RequireString(request, "subject") if err != nil { return b.handleError("get subject name", err), nil } @@ -360,13 +363,13 @@ func (b *KafkaSchemaRegistryToolBuilder) handleSchemaVersionsList(ctx context.Co } // handleSchemaVersionGet handles getting a specific version of a schema -func (b *KafkaSchemaRegistryToolBuilder) handleSchemaVersionGet(ctx context.Context, client *sr.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - subject, err := request.RequireString("subject") +func (b *KafkaSchemaRegistryToolBuilder) handleSchemaVersionGet(ctx context.Context, client *sr.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + subject, err := adapter.RequireString(request, "subject") if err != nil { return b.handleError("get subject name", err), nil } - versionStr, err := request.RequireString("version") + versionStr, err := adapter.RequireString(request, "version") if err != nil { return b.handleError("get version", err), nil } @@ -390,13 +393,13 @@ func (b *KafkaSchemaRegistryToolBuilder) handleSchemaVersionGet(ctx context.Cont } // handleSchemaVersionDelete handles deleting a specific version of a schema -func (b *KafkaSchemaRegistryToolBuilder) handleSchemaVersionDelete(ctx context.Context, client *sr.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - subject, err := request.RequireString("subject") +func (b *KafkaSchemaRegistryToolBuilder) handleSchemaVersionDelete(ctx context.Context, client *sr.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + subject, err := adapter.RequireString(request, "subject") if err != nil { return b.handleError("get subject name", err), nil } - versionStr, err := request.RequireString("version") + versionStr, err := adapter.RequireString(request, "version") if err != nil { return b.handleError("get version", err), nil } @@ -412,12 +415,12 @@ func (b *KafkaSchemaRegistryToolBuilder) handleSchemaVersionDelete(ctx context.C return b.handleError("delete schema version", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Schema version %d for subject %s deleted successfully", version, subject)), nil + return adapter.NewTextResult(fmt.Sprintf("Schema version %d for subject %s deleted successfully", version, subject)), nil } // handleSchemaCompatibilityGet handles getting compatibility setting -func (b *KafkaSchemaRegistryToolBuilder) handleSchemaCompatibilityGet(ctx context.Context, client *sr.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - subject := request.GetString("subject", "") // Optional for global compatibility +func (b *KafkaSchemaRegistryToolBuilder) handleSchemaCompatibilityGet(ctx context.Context, client *sr.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + subject := adapter.GetString(request, "subject", "") // Optional for global compatibility var results []sr.CompatibilityResult if subject != "" { @@ -440,17 +443,17 @@ func (b *KafkaSchemaRegistryToolBuilder) handleSchemaCompatibilityGet(ctx contex return b.marshalResponse(map[string]string{"compatibility": results[0].Level.String()}) } - return mcp.NewToolResultError("No compatibility result returned"), nil + return adapter.NewErrorResult("No compatibility result returned"), nil } // handleSchemaCompatibilitySet handles setting compatibility level -func (b *KafkaSchemaRegistryToolBuilder) handleSchemaCompatibilitySet(ctx context.Context, client *sr.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - compatibilityStr, err := request.RequireString("compatibility") +func (b *KafkaSchemaRegistryToolBuilder) handleSchemaCompatibilitySet(ctx context.Context, client *sr.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + compatibilityStr, err := adapter.RequireString(request, "compatibility") if err != nil { return b.handleError("get compatibility level", err), nil } - subject := request.GetString("subject", "") // Optional for global compatibility + subject := adapter.GetString(request, "subject", "") // Optional for global compatibility // Parse compatibility level var compatibility sr.CompatibilityLevel @@ -464,7 +467,7 @@ func (b *KafkaSchemaRegistryToolBuilder) handleSchemaCompatibilitySet(ctx contex case "NONE": compatibility = sr.CompatNone default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid compatibility level: %s. Valid levels: BACKWARD, FORWARD, FULL, NONE", compatibilityStr)), nil + return adapter.NewErrorResult("Invalid compatibility level: %s. Valid levels: BACKWARD, FORWARD, FULL, NONE", compatibilityStr), nil } // Create SetCompatibility request @@ -488,11 +491,11 @@ func (b *KafkaSchemaRegistryToolBuilder) handleSchemaCompatibilitySet(ctx contex } } - return mcp.NewToolResultText(fmt.Sprintf("Compatibility level set to %s", compatibilityStr)), nil + return adapter.NewTextResult(fmt.Sprintf("Compatibility level set to %s", compatibilityStr)), nil } // handleSchemaTypesList handles listing supported schema types -func (b *KafkaSchemaRegistryToolBuilder) handleSchemaTypesList(_ context.Context, _ *sr.Client, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *KafkaSchemaRegistryToolBuilder) handleSchemaTypesList(_ context.Context, _ *sr.Client, _ *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { types := []string{"AVRO", "JSON", "PROTOBUF"} return b.marshalResponse(types) } diff --git a/pkg/mcp/builders/kafka/topics.go b/pkg/mcp/builders/kafka/topics.go index 3960d54..38b0965 100644 --- a/pkg/mcp/builders/kafka/topics.go +++ b/pkg/mcp/builders/kafka/topics.go @@ -20,9 +20,9 @@ import ( "fmt" "strings" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" mcpCtx "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" "github.com/twmb/franz-go/pkg/kadm" ) @@ -55,7 +55,7 @@ func NewKafkaTopicsToolBuilder() *KafkaTopicsToolBuilder { } // BuildTools builds the Kafka Topics tool list -func (b *KafkaTopicsToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]server.ServerTool, error) { +func (b *KafkaTopicsToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]builders.ServerTool, error) { // Check features - return empty list if no required features are present if !b.HasAnyRequiredFeature(config.Features) { return nil, nil @@ -70,7 +70,7 @@ func (b *KafkaTopicsToolBuilder) BuildTools(_ context.Context, config builders.T tool := b.buildKafkaTopicsTool() handler := b.buildKafkaTopicsHandler(config.ReadOnly) - return []server.ServerTool{ + return []builders.ServerTool{ { Tool: tool, Handler: handler, @@ -79,7 +79,7 @@ func (b *KafkaTopicsToolBuilder) BuildTools(_ context.Context, config builders.T } // buildKafkaTopicsTool builds the Kafka Topics MCP tool definition -func (b *KafkaTopicsToolBuilder) buildKafkaTopicsTool() mcp.Tool { +func (b *KafkaTopicsToolBuilder) buildKafkaTopicsTool() *mcpsdk.Tool { resourceDesc := "Resource to operate on. Available resources:\n" + "- topic: A single Kafka topic for operations on individual topics (create, get, delete)\n" + "- topics: Collection of Kafka topics for bulk operations (list)" @@ -137,51 +137,51 @@ func (b *KafkaTopicsToolBuilder) buildKafkaTopicsTool() mcp.Tool { " name: \"old-topic\"\n\n" + "This tool requires appropriate Kafka permissions for topic management." - return mcp.NewTool("kafka_admin_topics", - mcp.WithDescription(toolDesc), - mcp.WithString("resource", mcp.Required(), - mcp.Description(resourceDesc), + return builders.NewTool("kafka_admin_topics", + builders.WithDescription(toolDesc), + builders.WithString("resource", builders.Required(), + builders.Description(resourceDesc), ), - mcp.WithString("operation", mcp.Required(), - mcp.Description(operationDesc), + builders.WithString("operation", builders.Required(), + builders.Description(operationDesc), ), - mcp.WithString("name", - mcp.Description("The name of the Kafka topic to operate on. "+ + builders.WithString("name", + builders.Description("The name of the Kafka topic to operate on. "+ "Required for 'get', 'create', 'delete', and 'metadata' operations on the 'topic' resource. "+ "Topic names should follow Kafka naming conventions (alphanumeric, dots, underscores, and hyphens).")), - mcp.WithNumber("partitions", - mcp.Description("The number of partitions for the topic. Required for 'create' operation. "+ + builders.WithNumber("partitions", + builders.Description("The number of partitions for the topic. Required for 'create' operation. "+ "Partitions determine the parallelism and scalability of the topic. "+ "More partitions allow more concurrent consumers and higher throughput.")), - mcp.WithNumber("replicationFactor", - mcp.Description("The replication factor for the topic. Required for 'create' operation. "+ + builders.WithNumber("replicationFactor", + builders.Description("The replication factor for the topic. Required for 'create' operation. "+ "Replication factor determines fault tolerance - it should be at least 2 for production use. "+ "Cannot exceed the number of available brokers in the cluster.")), - mcp.WithObject("configs", - mcp.Description("Optional configuration parameters for the topic during 'create' operation. "+ + builders.WithObject("configs", + builders.Description("Optional configuration parameters for the topic during 'create' operation. "+ "Common configurations include:\n"+ "- retention.ms: How long to retain messages (milliseconds)\n"+ "- compression.type: Compression algorithm (none, gzip, snappy, lz4, zstd)\n"+ "- cleanup.policy: Log cleanup policy (delete, compact, compact,delete)\n"+ "- segment.ms: Time before a new log segment is rolled out\n"+ "- max.message.bytes: Maximum size of a message batch")), - mcp.WithBoolean("includeInternal", - mcp.Description("Whether to include internal Kafka topics in the 'list' operation. "+ + builders.WithBoolean("includeInternal", + builders.Description("Whether to include internal Kafka topics in the 'list' operation. "+ "Internal topics are used by Kafka itself (e.g., __consumer_offsets, __transaction_state). "+ "Default: false")), ) } // buildKafkaTopicsHandler builds the Kafka Topics handler function -func (b *KafkaTopicsToolBuilder) buildKafkaTopicsHandler(readOnly bool) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *KafkaTopicsToolBuilder) buildKafkaTopicsHandler(readOnly bool) mcpsdk.ToolHandler { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - resource, err := request.RequireString("resource") + resource, err := adapter.RequireString(request, "resource") if err != nil { return b.handleError("get resource", err), nil } - operation, err := request.RequireString("operation") + operation, err := adapter.RequireString(request, "operation") if err != nil { return b.handleError("get operation", err), nil } @@ -192,7 +192,7 @@ func (b *KafkaTopicsToolBuilder) buildKafkaTopicsHandler(readOnly bool) func(con // Validate write operations in read-only mode if readOnly && (operation == "create" || operation == "delete") { - return mcp.NewToolResultError("Write operations are not allowed in read-only mode"), nil + return adapter.NewErrorResult("Write operations are not allowed in read-only mode"), nil } // Get Kafka admin client @@ -212,7 +212,7 @@ func (b *KafkaTopicsToolBuilder) buildKafkaTopicsHandler(readOnly bool) func(con case "list": return b.handleKafkaTopicsList(ctx, admin, request) default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid operation for resource 'topics': %s", operation)), nil + return adapter.NewErrorResult("Invalid operation for resource 'topics': %s", operation), nil } case "topic": switch operation { @@ -225,10 +225,10 @@ func (b *KafkaTopicsToolBuilder) buildKafkaTopicsHandler(readOnly bool) func(con case "metadata": return b.handleKafkaTopicMetadata(ctx, admin, request) default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid operation for resource 'topic': %s", operation)), nil + return adapter.NewErrorResult("Invalid operation for resource 'topic': %s", operation), nil } default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid resource: %s. Available resources: topics, topic", resource)), nil + return adapter.NewErrorResult("Invalid resource: %s. Available resources: topics, topic", resource), nil } } } @@ -236,22 +236,25 @@ func (b *KafkaTopicsToolBuilder) buildKafkaTopicsHandler(readOnly bool) func(con // Utility functions // handleError provides unified error handling -func (b *KafkaTopicsToolBuilder) handleError(operation string, err error) *mcp.CallToolResult { - return mcp.NewToolResultError(fmt.Sprintf("Failed to %s: %v", operation, err)) +func (b *KafkaTopicsToolBuilder) handleError(operation string, err error) *mcpsdk.CallToolResult { + if err != nil { + return adapter.NewErrorResult("Failed to %s: %v", operation, err) + } + return adapter.NewErrorResult("Failed to %s", operation) } // marshalResponse provides unified JSON serialization for responses -func (b *KafkaTopicsToolBuilder) marshalResponse(data interface{}) (*mcp.CallToolResult, error) { +func (b *KafkaTopicsToolBuilder) marshalResponse(data interface{}) (*mcpsdk.CallToolResult, error) { jsonBytes, err := json.Marshal(data) if err != nil { return b.handleError("marshal response", err), nil } - return mcp.NewToolResultText(string(jsonBytes)), nil + return adapter.NewTextResult(string(jsonBytes)), nil } // handleKafkaTopicsList handles listing all topics -func (b *KafkaTopicsToolBuilder) handleKafkaTopicsList(ctx context.Context, admin *kadm.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - includeInternal := request.GetBool("includeInternal", false) +func (b *KafkaTopicsToolBuilder) handleKafkaTopicsList(ctx context.Context, admin *kadm.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + includeInternal := adapter.GetBool(request, "includeInternal", false) topics, err := admin.ListTopics(ctx) if err != nil { @@ -273,8 +276,8 @@ func (b *KafkaTopicsToolBuilder) handleKafkaTopicsList(ctx context.Context, admi } // handleKafkaTopicGet handles getting detailed information about a specific topic -func (b *KafkaTopicsToolBuilder) handleKafkaTopicGet(ctx context.Context, admin *kadm.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - topicName, err := request.RequireString("name") +func (b *KafkaTopicsToolBuilder) handleKafkaTopicGet(ctx context.Context, admin *kadm.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + topicName, err := adapter.RequireString(request, "name") if err != nil { return b.handleError("get topic name", err), nil } @@ -288,18 +291,18 @@ func (b *KafkaTopicsToolBuilder) handleKafkaTopicGet(ctx context.Context, admin } // handleKafkaTopicCreate handles creating a new topic -func (b *KafkaTopicsToolBuilder) handleKafkaTopicCreate(ctx context.Context, admin *kadm.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - topicName, err := request.RequireString("name") +func (b *KafkaTopicsToolBuilder) handleKafkaTopicCreate(ctx context.Context, admin *kadm.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + topicName, err := adapter.RequireString(request, "name") if err != nil { return b.handleError("get topic name", err), nil } - partitionsNum, err := request.RequireInt("partitions") + partitionsNum, err := adapter.RequireInt(request, "partitions") if err != nil { return b.handleError("get partitions", err), nil } - replicationFactorNum, err := request.RequireInt("replicationFactor") + replicationFactorNum, err := adapter.RequireInt(request, "replicationFactor") if err != nil { return b.handleError("get replication factor", err), nil } @@ -311,17 +314,19 @@ func (b *KafkaTopicsToolBuilder) handleKafkaTopicCreate(ctx context.Context, adm // Parse optional configs var configs map[string]*string - arguments := request.GetArguments() - if configsParam, exists := arguments["configs"]; exists { - if configsMap, ok := configsParam.(map[string]interface{}); ok { - configs = make(map[string]*string) - for key, value := range configsMap { - if strValue, ok := value.(string); ok { - configs[key] = &strValue - } else { - // Convert non-string values to strings - strValue := fmt.Sprintf("%v", value) - configs[key] = &strValue + arguments, err := adapter.GetArgumentsMap(request) + if err == nil { + if configsParam, exists := arguments["configs"]; exists { + if configsMap, ok := configsParam.(map[string]interface{}); ok { + configs = make(map[string]*string) + for key, value := range configsMap { + if strValue, ok := value.(string); ok { + configs[key] = &strValue + } else { + // Convert non-string values to strings + strValue := fmt.Sprintf("%v", value) + configs[key] = &strValue + } } } } @@ -337,8 +342,8 @@ func (b *KafkaTopicsToolBuilder) handleKafkaTopicCreate(ctx context.Context, adm } // handleKafkaTopicDelete handles deleting a topic -func (b *KafkaTopicsToolBuilder) handleKafkaTopicDelete(ctx context.Context, admin *kadm.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - topicName, err := request.RequireString("name") +func (b *KafkaTopicsToolBuilder) handleKafkaTopicDelete(ctx context.Context, admin *kadm.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + topicName, err := adapter.RequireString(request, "name") if err != nil { return b.handleError("get topic name", err), nil } @@ -352,8 +357,8 @@ func (b *KafkaTopicsToolBuilder) handleKafkaTopicDelete(ctx context.Context, adm } // handleKafkaTopicMetadata handles getting metadata for a topic -func (b *KafkaTopicsToolBuilder) handleKafkaTopicMetadata(ctx context.Context, admin *kadm.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - topicName, err := request.RequireString("name") +func (b *KafkaTopicsToolBuilder) handleKafkaTopicMetadata(ctx context.Context, admin *kadm.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + topicName, err := adapter.RequireString(request, "name") if err != nil { return b.handleError("get topic name", err), nil } diff --git a/pkg/mcp/builders/pulsar/brokers.go b/pkg/mcp/builders/pulsar/brokers.go index 233b1fb..3e7609c 100644 --- a/pkg/mcp/builders/pulsar/brokers.go +++ b/pkg/mcp/builders/pulsar/brokers.go @@ -19,10 +19,10 @@ import ( "encoding/json" "fmt" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/pulsarctl/pkg/cmdutils" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" mcpCtx "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" ) @@ -55,7 +55,7 @@ func NewPulsarAdminBrokersToolBuilder() *PulsarAdminBrokersToolBuilder { } // BuildTools builds the Pulsar admin brokers tool list -func (b *PulsarAdminBrokersToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]server.ServerTool, error) { +func (b *PulsarAdminBrokersToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]builders.ServerTool, error) { // Check features - return empty list if no required features are present if !b.HasAnyRequiredFeature(config.Features) { return nil, nil @@ -70,7 +70,7 @@ func (b *PulsarAdminBrokersToolBuilder) BuildTools(_ context.Context, config bui tool := b.buildPulsarAdminBrokersTool() handler := b.buildPulsarAdminBrokersHandler(config.ReadOnly) - return []server.ServerTool{ + return []builders.ServerTool{ { Tool: tool, Handler: handler, @@ -79,9 +79,9 @@ func (b *PulsarAdminBrokersToolBuilder) BuildTools(_ context.Context, config bui } // buildPulsarAdminBrokersTool builds the Pulsar admin brokers MCP tool definition -func (b *PulsarAdminBrokersToolBuilder) buildPulsarAdminBrokersTool() mcp.Tool { - return mcp.NewTool("pulsar_admin_brokers", - mcp.WithDescription("Unified tool for managing Apache Pulsar broker resources. This tool integrates multiple broker management functions, including:\n"+ +func (b *PulsarAdminBrokersToolBuilder) buildPulsarAdminBrokersTool() *mcpsdk.Tool { + return builders.NewTool("pulsar_admin_brokers", + builders.WithDescription("Unified tool for managing Apache Pulsar broker resources. This tool integrates multiple broker management functions, including:\n"+ "1. List active brokers in a cluster (resource=brokers, operation=list)\n"+ "2. Check broker health status (resource=health, operation=get)\n"+ "3. Manage broker configurations (resource=config, operation=get/update/delete)\n"+ @@ -89,80 +89,80 @@ func (b *PulsarAdminBrokersToolBuilder) buildPulsarAdminBrokersTool() mcp.Tool { "Different functions are accessed by combining resource and operation parameters, with other parameters used selectively based on operation type.\n"+ "Example: {\"resource\": \"config\", \"operation\": \"get\", \"configType\": \"dynamic\"} retrieves all dynamic configuration names.\n"+ "This tool requires Pulsar super-user permissions."), - mcp.WithString("resource", mcp.Required(), - mcp.Description("Type of resource to access, available options:\n"+ + builders.WithString("resource", builders.Required(), + builders.Description("Type of resource to access, available options:\n"+ "- brokers: Manage broker listings\n"+ "- health: Check broker health status\n"+ "- config: Manage broker configurations\n"+ "- namespaces: Manage namespaces owned by a broker"), ), - mcp.WithString("operation", mcp.Required(), - mcp.Description("Operation to perform, available options:\n"+ + builders.WithString("operation", builders.Required(), + builders.Description("Operation to perform, available options:\n"+ "- list: List resources (used with brokers)\n"+ "- get: Retrieve resource information (used with health, config, namespaces)\n"+ "- update: Update a resource (used with config)\n"+ "- delete: Delete a resource (used with config)"), ), - mcp.WithString("clusterName", - mcp.Description("Pulsar cluster name, required for these operations:\n"+ + builders.WithString("clusterName", + builders.Description("Pulsar cluster name, required for these operations:\n"+ "- When resource=brokers, operation=list\n"+ "- When resource=namespaces, operation=get"), ), - mcp.WithString("brokerUrl", - mcp.Description("Broker URL, such as '127.0.0.1:8080', required for these operations:\n"+ + builders.WithString("brokerUrl", + builders.Description("Broker URL, such as '127.0.0.1:8080', required for these operations:\n"+ "- When resource=namespaces, operation=get"), ), - mcp.WithString("configType", - mcp.Description("Configuration type, required when resource=config, operation=get, available options:\n"+ + builders.WithString("configType", + builders.Description("Configuration type, required when resource=config, operation=get, available options:\n"+ "- dynamic: Get list of dynamically modifiable configuration names\n"+ "- runtime: Get all runtime configurations (including static and dynamic configs)\n"+ "- internal: Get internal configuration information\n"+ "- all_dynamic: Get all dynamic configurations and their current values"), ), - mcp.WithString("configName", - mcp.Description("Configuration parameter name, required for these operations:\n"+ + builders.WithString("configName", + builders.Description("Configuration parameter name, required for these operations:\n"+ "- When resource=config, operation=update\n"+ "- When resource=config, operation=delete"), ), - mcp.WithString("configValue", - mcp.Description("Configuration parameter value, required for these operations:\n"+ + builders.WithString("configValue", + builders.Description("Configuration parameter value, required for these operations:\n"+ "- When resource=config, operation=update"), ), ) } // buildPulsarAdminBrokersHandler builds the Pulsar admin brokers handler function -func (b *PulsarAdminBrokersToolBuilder) buildPulsarAdminBrokersHandler(readOnly bool) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminBrokersToolBuilder) buildPulsarAdminBrokersHandler(readOnly bool) func(context.Context, *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get Pulsar session from context session := mcpCtx.GetPulsarSession(ctx) if session == nil { - return mcp.NewToolResultError("Pulsar session not found in context"), nil + return adapter.NewErrorResult("Pulsar session not found in context"), nil } // Get admin client client, err := session.GetAdminClient() if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get admin client: %v", err)), nil + return adapter.NewErrorResult("Failed to get admin client: %v", err), nil } // Get required parameters - resource, err := request.RequireString("resource") + resource, err := adapter.RequireString(request, "resource") if err != nil { - return mcp.NewToolResultError("Missing required resource parameter. " + + return adapter.NewErrorResult("Missing required resource parameter. " + "Please specify one of: brokers, health, config, namespaces."), nil } - operation, err := request.RequireString("operation") + operation, err := adapter.RequireString(request, "operation") if err != nil { - return mcp.NewToolResultError("Missing required operation parameter. " + + return adapter.NewErrorResult("Missing required operation parameter. " + "Please specify one of: list, get, update, delete based on the resource type."), nil } // Validate if the parameter combination is valid validCombination, errMsg := b.validateResourceOperation(resource, operation) if !validCombination { - return mcp.NewToolResultError(errMsg), nil + return adapter.NewErrorResult(errMsg), nil } // Process request based on resource type @@ -174,14 +174,14 @@ func (b *PulsarAdminBrokersToolBuilder) buildPulsarAdminBrokersHandler(readOnly case "config": // Check write operation permissions if (operation == "update" || operation == "delete") && readOnly { - return mcp.NewToolResultError("Configuration update/delete operations not allowed in read-only mode. " + + return adapter.NewErrorResult("Configuration update/delete operations not allowed in read-only mode. " + "Please contact your administrator if you need to modify broker configurations."), nil } return b.handleConfigResource(client, operation, request) case "namespaces": return b.handleNamespacesResource(client, operation, request) default: - return mcp.NewToolResultError(fmt.Sprintf("Unsupported resource: %s. "+ + return adapter.NewErrorResult(fmt.Sprintf("Unsupported resource: %s. "+ "Please use one of: brokers, health, config, namespaces.", resource)), nil } } @@ -212,57 +212,57 @@ func (b *PulsarAdminBrokersToolBuilder) validateResourceOperation(resource, oper } // handleBrokersResource handles brokers resource -func (b *PulsarAdminBrokersToolBuilder) handleBrokersResource(client cmdutils.Client, operation string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminBrokersToolBuilder) handleBrokersResource(client cmdutils.Client, operation string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { switch operation { case "list": - clusterName, err := request.RequireString("clusterName") + clusterName, err := adapter.RequireString(request, "clusterName") if err != nil { - return mcp.NewToolResultError("Missing required parameter 'clusterName'. " + + return adapter.NewErrorResult("Missing required parameter 'clusterName'. " + "Please provide the name of the Pulsar cluster to list brokers for."), nil } brokers, err := client.Brokers().GetActiveBrokers(clusterName) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get active brokers: %v. "+ + return adapter.NewErrorResult(fmt.Sprintf("Failed to get active brokers: %v. "+ "Please verify the cluster name and ensure the Pulsar service is running.", err)), nil } brokersJSON, err := json.Marshal(brokers) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to serialize brokers list: %v", err)), nil + return adapter.NewErrorResult("Failed to serialize brokers list: %v", err), nil } - return mcp.NewToolResultText(string(brokersJSON)), nil + return adapter.NewTextResult(string(brokersJSON)), nil default: - return mcp.NewToolResultError(fmt.Sprintf("Unsupported operation '%s' for brokers resource. "+ + return adapter.NewErrorResult(fmt.Sprintf("Unsupported operation '%s' for brokers resource. "+ "The only supported operation is 'list'.", operation)), nil } } // handleHealthResource handles health resource -func (b *PulsarAdminBrokersToolBuilder) handleHealthResource(client cmdutils.Client, operation string, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminBrokersToolBuilder) handleHealthResource(client cmdutils.Client, operation string, _ *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { switch operation { case "get": //nolint:staticcheck err := client.Brokers().HealthCheck() if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Broker health check failed: %v. "+ + return adapter.NewErrorResult(fmt.Sprintf("Broker health check failed: %v. "+ "The broker might be down or experiencing issues.", err)), nil } - return mcp.NewToolResultText("ok"), nil + return adapter.NewTextResult("ok"), nil default: - return mcp.NewToolResultError(fmt.Sprintf("Unsupported operation '%s' for health resource. "+ + return adapter.NewErrorResult(fmt.Sprintf("Unsupported operation '%s' for health resource. "+ "The only supported operation is 'get'.", operation)), nil } } // handleConfigResource handles config resource -func (b *PulsarAdminBrokersToolBuilder) handleConfigResource(client cmdutils.Client, operation string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminBrokersToolBuilder) handleConfigResource(client cmdutils.Client, operation string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { switch operation { case "get": - configType, err := request.RequireString("configType") + configType, err := adapter.RequireString(request, "configType") if err != nil { - return mcp.NewToolResultError("Missing required parameter 'configType'. " + + return adapter.NewErrorResult("Missing required parameter 'configType'. " + "Please specify one of: dynamic, runtime, internal, all_dynamic."), nil } @@ -279,95 +279,95 @@ func (b *PulsarAdminBrokersToolBuilder) handleConfigResource(client cmdutils.Cli case "all_dynamic": result, fetchErr = client.Brokers().GetAllDynamicConfigurations() default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid config type: '%s'. "+ + return adapter.NewErrorResult(fmt.Sprintf("Invalid config type: '%s'. "+ "Valid types are: dynamic, runtime, internal, all_dynamic.", configType)), nil } if fetchErr != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get %s configuration: %v", configType, fetchErr)), nil + return adapter.NewErrorResult("Failed to get %s configuration: %v", configType, fetchErr), nil } resultJSON, err := json.Marshal(result) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to serialize configuration: %v", err)), nil + return adapter.NewErrorResult("Failed to serialize configuration: %v", err), nil } - return mcp.NewToolResultText(string(resultJSON)), nil + return adapter.NewTextResult(string(resultJSON)), nil case "update": - configName, err := request.RequireString("configName") + configName, err := adapter.RequireString(request, "configName") if err != nil { - return mcp.NewToolResultError("Missing required parameter 'configName'. " + + return adapter.NewErrorResult("Missing required parameter 'configName'. " + "Please provide the name of the configuration parameter to update."), nil } - configValue, err := request.RequireString("configValue") + configValue, err := adapter.RequireString(request, "configValue") if err != nil { - return mcp.NewToolResultError("Missing required parameter 'configValue'. " + + return adapter.NewErrorResult("Missing required parameter 'configValue'. " + "Please provide the new value for the configuration parameter."), nil } err = client.Brokers().UpdateDynamicConfiguration(configName, configValue) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to update configuration: %v. "+ + return adapter.NewErrorResult(fmt.Sprintf("Failed to update configuration: %v. "+ "Please verify the configuration name is valid and the value is of the correct type.", err)), nil } - return mcp.NewToolResultText(fmt.Sprintf("Dynamic configuration '%s' updated successfully to '%s'", + return adapter.NewTextResult(fmt.Sprintf("Dynamic configuration '%s' updated successfully to '%s'", configName, configValue)), nil case "delete": - configName, err := request.RequireString("configName") + configName, err := adapter.RequireString(request, "configName") if err != nil { - return mcp.NewToolResultError("Missing required parameter 'configName'. " + + return adapter.NewErrorResult("Missing required parameter 'configName'. " + "Please provide the name of the configuration parameter to delete."), nil } err = client.Brokers().DeleteDynamicConfiguration(configName) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to delete configuration: %v. "+ + return adapter.NewErrorResult(fmt.Sprintf("Failed to delete configuration: %v. "+ "Please verify the configuration name is valid and exists.", err)), nil } - return mcp.NewToolResultText(fmt.Sprintf("Dynamic configuration '%s' deleted successfully", configName)), nil + return adapter.NewTextResult(fmt.Sprintf("Dynamic configuration '%s' deleted successfully", configName)), nil default: - return mcp.NewToolResultError(fmt.Sprintf("Unsupported operation '%s' for config resource. "+ + return adapter.NewErrorResult(fmt.Sprintf("Unsupported operation '%s' for config resource. "+ "Supported operations are: get, update, delete.", operation)), nil } } // handleNamespacesResource handles namespaces resource -func (b *PulsarAdminBrokersToolBuilder) handleNamespacesResource(client cmdutils.Client, operation string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminBrokersToolBuilder) handleNamespacesResource(client cmdutils.Client, operation string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { switch operation { case "get": - clusterName, err := request.RequireString("clusterName") + clusterName, err := adapter.RequireString(request, "clusterName") if err != nil { - return mcp.NewToolResultError("Missing required parameter 'clusterName'. " + + return adapter.NewErrorResult("Missing required parameter 'clusterName'. " + "Please provide the name of the Pulsar cluster."), nil } - brokerURL, err := request.RequireString("brokerUrl") + brokerURL, err := adapter.RequireString(request, "brokerUrl") if err != nil { - return mcp.NewToolResultError("Missing required parameter 'brokerUrl'. " + + return adapter.NewErrorResult("Missing required parameter 'brokerUrl'. " + "Please provide the URL of the broker (e.g., '127.0.0.1:8080')."), nil } namespaces, err := client.Brokers().GetOwnedNamespaces(clusterName, brokerURL) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get owned namespaces: %v. "+ + return adapter.NewErrorResult(fmt.Sprintf("Failed to get owned namespaces: %v. "+ "Please verify the cluster name and broker URL are correct.", err)), nil } namespacesJSON, err := json.Marshal(namespaces) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to serialize namespaces: %v", err)), nil + return adapter.NewErrorResult("Failed to serialize namespaces: %v", err), nil } - return mcp.NewToolResultText(string(namespacesJSON)), nil + return adapter.NewTextResult(string(namespacesJSON)), nil default: - return mcp.NewToolResultError(fmt.Sprintf("Unsupported operation '%s' for namespaces resource. "+ + return adapter.NewErrorResult(fmt.Sprintf("Unsupported operation '%s' for namespaces resource. "+ "The only supported operation is 'get'.", operation)), nil } } diff --git a/pkg/mcp/builders/pulsar/brokers_stats.go b/pkg/mcp/builders/pulsar/brokers_stats.go index b0da1df..f93ed3e 100644 --- a/pkg/mcp/builders/pulsar/brokers_stats.go +++ b/pkg/mcp/builders/pulsar/brokers_stats.go @@ -19,10 +19,10 @@ import ( "encoding/json" "fmt" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/pulsarctl/pkg/cmdutils" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" mcpCtx "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" ) @@ -55,7 +55,7 @@ func NewPulsarAdminBrokerStatsToolBuilder() *PulsarAdminBrokerStatsToolBuilder { } // BuildTools builds the Pulsar Admin Broker Stats tool list -func (b *PulsarAdminBrokerStatsToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]server.ServerTool, error) { +func (b *PulsarAdminBrokerStatsToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]builders.ServerTool, error) { // Check features - return empty list if no required features are present if !b.HasAnyRequiredFeature(config.Features) { return nil, nil @@ -70,7 +70,7 @@ func (b *PulsarAdminBrokerStatsToolBuilder) BuildTools(_ context.Context, config tool := b.buildBrokerStatsTool() handler := b.buildBrokerStatsHandler(config.ReadOnly) - return []server.ServerTool{ + return []builders.ServerTool{ { Tool: tool, Handler: handler, @@ -79,7 +79,7 @@ func (b *PulsarAdminBrokerStatsToolBuilder) BuildTools(_ context.Context, config } // buildBrokerStatsTool builds the Pulsar Admin Broker Stats MCP tool definition -func (b *PulsarAdminBrokerStatsToolBuilder) buildBrokerStatsTool() mcp.Tool { +func (b *PulsarAdminBrokerStatsToolBuilder) buildBrokerStatsTool() *mcpsdk.Tool { resourceDesc := "Type of broker stats resource to access, available options:\n" + "- monitoring_metrics: Metrics for the broker's monitoring system\n" + "- mbeans: JVM MBeans statistics\n" + @@ -98,24 +98,24 @@ func (b *PulsarAdminBrokerStatsToolBuilder) buildBrokerStatsTool() mcp.Tool { "Example: {\"resource\": \"allocator_stats\", \"allocator_name\": \"default\"} retrieves stats for the default allocator\n" + "This tool requires Pulsar super-user permissions." - return mcp.NewTool("pulsar_admin_broker_stats", - mcp.WithDescription(toolDesc), - mcp.WithString("resource", mcp.Required(), - mcp.Description(resourceDesc), + return builders.NewTool("pulsar_admin_broker_stats", + builders.WithDescription(toolDesc), + builders.WithString("resource", builders.Required(), + builders.Description(resourceDesc), ), - mcp.WithString("allocator_name", - mcp.Description("The name of the allocator to get statistics for. Required only when resource=allocator_stats"), + builders.WithString("allocator_name", + builders.Description("The name of the allocator to get statistics for. Required only when resource=allocator_stats"), ), ) } // buildBrokerStatsHandler builds the Pulsar Admin Broker Stats handler function -func (b *PulsarAdminBrokerStatsToolBuilder) buildBrokerStatsHandler(_ bool) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminBrokerStatsToolBuilder) buildBrokerStatsHandler(_ bool) func(context.Context, *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get Pulsar admin client session := mcpCtx.GetPulsarSession(ctx) if session == nil { - return mcp.NewToolResultError("Pulsar session not found in context"), nil + return adapter.NewErrorResult("Pulsar session not found in context"), nil } client, err := session.GetAdminClient() if err != nil { @@ -123,9 +123,9 @@ func (b *PulsarAdminBrokerStatsToolBuilder) buildBrokerStatsHandler(_ bool) func } // Get required resource parameter - resource, err := request.RequireString("resource") + resource, err := adapter.RequireString(request, "resource") if err != nil { - return mcp.NewToolResultError("Missing required parameter 'resource'. " + + return adapter.NewErrorResult("Missing required parameter 'resource'. " + "Please specify one of: monitoring_metrics, mbeans, topics, allocator_stats, load_report."), nil } @@ -138,16 +138,16 @@ func (b *PulsarAdminBrokerStatsToolBuilder) buildBrokerStatsHandler(_ bool) func case "topics": return b.handleTopics(client) case "allocator_stats": - allocatorName, err := request.RequireString("allocator_name") + allocatorName, err := adapter.RequireString(request, "allocator_name") if err != nil { - return mcp.NewToolResultError("Missing required parameter 'allocator_name' for allocator_stats resource. " + + return adapter.NewErrorResult("Missing required parameter 'allocator_name' for allocator_stats resource. " + "Please provide the name of the allocator to get statistics for."), nil } return b.handleAllocatorStats(client, allocatorName) case "load_report": return b.handleLoadReport(client) default: - return mcp.NewToolResultError(fmt.Sprintf("Unsupported resource: %s. "+ + return adapter.NewErrorResult(fmt.Sprintf("Unsupported resource: %s. "+ "Please use one of: monitoring_metrics, mbeans, topics, allocator_stats, load_report.", resource)), nil } } @@ -156,23 +156,23 @@ func (b *PulsarAdminBrokerStatsToolBuilder) buildBrokerStatsHandler(_ bool) func // Utility functions // handleError provides unified error handling -func (b *PulsarAdminBrokerStatsToolBuilder) handleError(operation string, err error) *mcp.CallToolResult { - return mcp.NewToolResultError(fmt.Sprintf("Failed to %s: %v", operation, err)) +func (b *PulsarAdminBrokerStatsToolBuilder) handleError(operation string, err error) *mcpsdk.CallToolResult { + return adapter.NewErrorResult("Failed to %s: %v", operation, err) } // marshalResponse provides unified JSON serialization for responses -func (b *PulsarAdminBrokerStatsToolBuilder) marshalResponse(data interface{}) (*mcp.CallToolResult, error) { +func (b *PulsarAdminBrokerStatsToolBuilder) marshalResponse(data interface{}) (*mcpsdk.CallToolResult, error) { jsonBytes, err := json.Marshal(data) if err != nil { return b.handleError("marshal response", err), nil } - return mcp.NewToolResultText(string(jsonBytes)), nil + return adapter.NewTextResult(string(jsonBytes)), nil } // Specific operation handler functions // handleMonitoringMetrics handles retrieving monitoring metrics -func (b *PulsarAdminBrokerStatsToolBuilder) handleMonitoringMetrics(client cmdutils.Client) (*mcp.CallToolResult, error) { +func (b *PulsarAdminBrokerStatsToolBuilder) handleMonitoringMetrics(client cmdutils.Client) (*mcpsdk.CallToolResult, error) { stats, err := client.BrokerStats().GetMetrics() if err != nil { return b.handleError("get monitoring metrics", err), nil @@ -181,7 +181,7 @@ func (b *PulsarAdminBrokerStatsToolBuilder) handleMonitoringMetrics(client cmdut } // handleMBeans handles retrieving MBeans statistics -func (b *PulsarAdminBrokerStatsToolBuilder) handleMBeans(client cmdutils.Client) (*mcp.CallToolResult, error) { +func (b *PulsarAdminBrokerStatsToolBuilder) handleMBeans(client cmdutils.Client) (*mcpsdk.CallToolResult, error) { stats, err := client.BrokerStats().GetMBeans() if err != nil { return b.handleError("get MBeans", err), nil @@ -190,7 +190,7 @@ func (b *PulsarAdminBrokerStatsToolBuilder) handleMBeans(client cmdutils.Client) } // handleTopics handles retrieving topics statistics -func (b *PulsarAdminBrokerStatsToolBuilder) handleTopics(client cmdutils.Client) (*mcp.CallToolResult, error) { +func (b *PulsarAdminBrokerStatsToolBuilder) handleTopics(client cmdutils.Client) (*mcpsdk.CallToolResult, error) { stats, err := client.BrokerStats().GetTopics() if err != nil { return b.handleError("get topics stats", err), nil @@ -199,7 +199,7 @@ func (b *PulsarAdminBrokerStatsToolBuilder) handleTopics(client cmdutils.Client) } // handleAllocatorStats handles retrieving allocator statistics -func (b *PulsarAdminBrokerStatsToolBuilder) handleAllocatorStats(client cmdutils.Client, allocatorName string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminBrokerStatsToolBuilder) handleAllocatorStats(client cmdutils.Client, allocatorName string) (*mcpsdk.CallToolResult, error) { stats, err := client.BrokerStats().GetAllocatorStats(allocatorName) if err != nil { return b.handleError("get allocator stats", err), nil @@ -208,7 +208,7 @@ func (b *PulsarAdminBrokerStatsToolBuilder) handleAllocatorStats(client cmdutils } // handleLoadReport handles retrieving load report -func (b *PulsarAdminBrokerStatsToolBuilder) handleLoadReport(client cmdutils.Client) (*mcp.CallToolResult, error) { +func (b *PulsarAdminBrokerStatsToolBuilder) handleLoadReport(client cmdutils.Client) (*mcpsdk.CallToolResult, error) { stats, err := client.BrokerStats().GetLoadReport() if err != nil { return b.handleError("get load report", err), nil diff --git a/pkg/mcp/builders/pulsar/cluster.go b/pkg/mcp/builders/pulsar/cluster.go index 02ce2e6..1baea4c 100644 --- a/pkg/mcp/builders/pulsar/cluster.go +++ b/pkg/mcp/builders/pulsar/cluster.go @@ -20,10 +20,10 @@ import ( "fmt" "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/pulsarctl/pkg/cmdutils" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" mcpCtx "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" ) @@ -58,7 +58,7 @@ func NewPulsarAdminClusterToolBuilder() *PulsarAdminClusterToolBuilder { // BuildTools builds the Pulsar Admin Cluster tool list // This is the core method implementing the ToolBuilder interface -func (b *PulsarAdminClusterToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]server.ServerTool, error) { +func (b *PulsarAdminClusterToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]builders.ServerTool, error) { // Check features - return empty list if no required features are present if !b.HasAnyRequiredFeature(config.Features) { return nil, nil @@ -73,7 +73,7 @@ func (b *PulsarAdminClusterToolBuilder) BuildTools(_ context.Context, config bui tool := b.buildClusterTool() handler := b.buildClusterHandler(config.ReadOnly) - return []server.ServerTool{ + return []builders.ServerTool{ { Tool: tool, Handler: handler, @@ -83,7 +83,7 @@ func (b *PulsarAdminClusterToolBuilder) BuildTools(_ context.Context, config bui // buildClusterTool builds the Pulsar Admin Cluster MCP tool definition // Migrated from the original tool definition logic -func (b *PulsarAdminClusterToolBuilder) buildClusterTool() mcp.Tool { +func (b *PulsarAdminClusterToolBuilder) buildClusterTool() *mcpsdk.Tool { toolDesc := "Unified tool for managing Apache Pulsar clusters.\n" + "This tool provides access to various cluster resources and operations, including:\n" + "1. Manage clusters (resource=cluster): List, get, create, update, delete clusters\n" + @@ -108,46 +108,46 @@ func (b *PulsarAdminClusterToolBuilder) buildClusterTool() mcp.Tool { "- update: Update an existing resource (used with cluster, peer_clusters, failure_domain)\n" + "- delete: Delete a resource (used with cluster, failure_domain)" - return mcp.NewTool("pulsar_admin_cluster", - mcp.WithDescription(toolDesc), - mcp.WithString("resource", mcp.Required(), - mcp.Description(resourceDesc), + return builders.NewTool("pulsar_admin_cluster", + builders.WithDescription(toolDesc), + builders.WithString("resource", builders.Required(), + builders.Description(resourceDesc), ), - mcp.WithString("operation", mcp.Required(), - mcp.Description(operationDesc), + builders.WithString("operation", builders.Required(), + builders.Description(operationDesc), ), - mcp.WithString("cluster_name", - mcp.Description("Name of the Pulsar cluster, required for all operations except 'list' with resource=cluster"), + builders.WithString("cluster_name", + builders.Description("Name of the Pulsar cluster, required for all operations except 'list' with resource=cluster"), ), - mcp.WithString("domain_name", - mcp.Description("Name of the failure domain, required when resource=failure_domain and operation is get, create, update, or delete"), + builders.WithString("domain_name", + builders.Description("Name of the failure domain, required when resource=failure_domain and operation is get, create, update, or delete"), ), - mcp.WithString("service_url", - mcp.Description("Pulsar cluster web service URL (e.g., http://example.pulsar.io:8080), used when resource=cluster and operation is create or update"), + builders.WithString("service_url", + builders.Description("Pulsar cluster web service URL (e.g., http://example.pulsar.io:8080), used when resource=cluster and operation is create or update"), ), - mcp.WithString("service_url_tls", - mcp.Description("Pulsar cluster TLS secured web service URL (e.g., https://example.pulsar.io:8443), used when resource=cluster and operation is create or update"), + builders.WithString("service_url_tls", + builders.Description("Pulsar cluster TLS secured web service URL (e.g., https://example.pulsar.io:8443), used when resource=cluster and operation is create or update"), ), - mcp.WithString("broker_service_url", - mcp.Description("Pulsar cluster broker service URL (e.g., pulsar://example.pulsar.io:6650), used when resource=cluster and operation is create or update"), + builders.WithString("broker_service_url", + builders.Description("Pulsar cluster broker service URL (e.g., pulsar://example.pulsar.io:6650), used when resource=cluster and operation is create or update"), ), - mcp.WithString("broker_service_url_tls", - mcp.Description("Pulsar cluster TLS secured broker service URL (e.g., pulsar+ssl://example.pulsar.io:6651), used when resource=cluster and operation is create or update"), + builders.WithString("broker_service_url_tls", + builders.Description("Pulsar cluster TLS secured broker service URL (e.g., pulsar+ssl://example.pulsar.io:6651), used when resource=cluster and operation is create or update"), ), - mcp.WithArray("peer_cluster_names", - mcp.Description("List of clusters to be registered as peer-clusters, used when:\n"+ + builders.WithArray("peer_cluster_names", + builders.Description("List of clusters to be registered as peer-clusters, used when:\n"+ "- resource=cluster and operation is create or update\n"+ "- resource=peer_clusters and operation is update"), - mcp.Items( + builders.Items( map[string]interface{}{ "type": "string", "description": "peer cluster name", }, ), ), - mcp.WithArray("brokers", - mcp.Description("List of broker names to include in a failure domain, required when resource=failure_domain and operation is create or update"), - mcp.Items( + builders.WithArray("brokers", + builders.Description("List of broker names to include in a failure domain, required when resource=failure_domain and operation is create or update"), + builders.Items( map[string]interface{}{ "type": "string", "description": "broker", @@ -159,12 +159,12 @@ func (b *PulsarAdminClusterToolBuilder) buildClusterTool() mcp.Tool { // buildClusterHandler builds the Pulsar Admin Cluster handler function // Migrated from the original handler logic -func (b *PulsarAdminClusterToolBuilder) buildClusterHandler(readOnly bool) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminClusterToolBuilder) buildClusterHandler(readOnly bool) func(context.Context, *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get Pulsar session from context session := mcpCtx.GetPulsarSession(ctx) if session == nil { - return mcp.NewToolResultError("Pulsar session not found in context"), nil + return adapter.NewErrorResult("Pulsar session not found in context"), nil } client, err := session.GetAdminClient() @@ -173,27 +173,27 @@ func (b *PulsarAdminClusterToolBuilder) buildClusterHandler(readOnly bool) func( } // Get required parameters - resource, err := request.RequireString("resource") + resource, err := adapter.RequireString(request, "resource") if err != nil { - return mcp.NewToolResultError("Missing required resource parameter. " + + return adapter.NewErrorResult("Missing required resource parameter. " + "Please specify one of: cluster, peer_clusters, failure_domain."), nil } - operation, err := request.RequireString("operation") + operation, err := adapter.RequireString(request, "operation") if err != nil { - return mcp.NewToolResultError("Missing required operation parameter. " + + return adapter.NewErrorResult("Missing required operation parameter. " + "Please specify one of: list, get, create, update, delete based on the resource type."), nil } // Validate if the parameter combination is valid validCombination, errMsg := b.validateClusterResourceOperation(resource, operation) if !validCombination { - return mcp.NewToolResultError(errMsg), nil + return adapter.NewErrorResult(errMsg), nil } // Check write operation permissions if (operation == "create" || operation == "update" || operation == "delete") && readOnly { - return mcp.NewToolResultError("Create/update/delete operations not allowed in read-only mode. " + + return adapter.NewErrorResult("Create/update/delete operations not allowed in read-only mode. " + "Please contact your administrator if you need to modify cluster resources."), nil } @@ -206,7 +206,7 @@ func (b *PulsarAdminClusterToolBuilder) buildClusterHandler(readOnly bool) func( case "failure_domain": return b.handleFailureDomainResource(client, operation, request) default: - return mcp.NewToolResultError(fmt.Sprintf("Unsupported resource: %s. "+ + return adapter.NewErrorResult(fmt.Sprintf("Unsupported resource: %s. "+ "Please use one of: cluster, peer_clusters, failure_domain.", resource)), nil } } @@ -215,17 +215,17 @@ func (b *PulsarAdminClusterToolBuilder) buildClusterHandler(readOnly bool) func( // Unified error handling and utility functions // handleError provides unified error handling -func (b *PulsarAdminClusterToolBuilder) handleError(operation string, err error) *mcp.CallToolResult { - return mcp.NewToolResultError(fmt.Sprintf("Failed to %s: %v", operation, err)) +func (b *PulsarAdminClusterToolBuilder) handleError(operation string, err error) *mcpsdk.CallToolResult { + return adapter.NewErrorResult("Failed to %s: %v", operation, err) } // marshalResponse provides unified JSON serialization for responses -func (b *PulsarAdminClusterToolBuilder) marshalResponse(data interface{}) (*mcp.CallToolResult, error) { +func (b *PulsarAdminClusterToolBuilder) marshalResponse(data interface{}) (*mcpsdk.CallToolResult, error) { jsonBytes, err := json.Marshal(data) if err != nil { return b.handleError("marshal response", err), nil } - return mcp.NewToolResultText(string(jsonBytes)), nil + return adapter.NewTextResult(string(jsonBytes)), nil } // Validate if the resource and operation combination is valid @@ -250,14 +250,14 @@ func (b *PulsarAdminClusterToolBuilder) validateClusterResourceOperation(resourc } // Handle cluster resource operations -func (b *PulsarAdminClusterToolBuilder) handleClusterResource(client cmdutils.Client, operation string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminClusterToolBuilder) handleClusterResource(client cmdutils.Client, operation string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { switch operation { case "list": return b.handleClusterList(client) case "get": - clusterName, err := request.RequireString("cluster_name") + clusterName, err := adapter.RequireString(request, "cluster_name") if err != nil { - return mcp.NewToolResultError("Missing required parameter 'cluster_name'. " + + return adapter.NewErrorResult("Missing required parameter 'cluster_name'. " + "Please provide the name of the cluster to get information for."), nil } return b.getClusterData(client, clusterName) @@ -266,22 +266,22 @@ func (b *PulsarAdminClusterToolBuilder) handleClusterResource(client cmdutils.Cl case "update": return b.updateCluster(client, request) case "delete": - clusterName, err := request.RequireString("cluster_name") + clusterName, err := adapter.RequireString(request, "cluster_name") if err != nil { - return mcp.NewToolResultError("Missing required parameter 'cluster_name'. " + + return adapter.NewErrorResult("Missing required parameter 'cluster_name'. " + "Please provide the name of the cluster to delete."), nil } return b.deleteCluster(client, clusterName) default: - return mcp.NewToolResultError(fmt.Sprintf("Unsupported cluster operation: %s", operation)), nil + return adapter.NewErrorResult("Unsupported cluster operation: %s", operation), nil } } // Handle peer clusters resource operations -func (b *PulsarAdminClusterToolBuilder) handlePeerClustersResource(client cmdutils.Client, operation string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - clusterName, err := request.RequireString("cluster_name") +func (b *PulsarAdminClusterToolBuilder) handlePeerClustersResource(client cmdutils.Client, operation string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + clusterName, err := adapter.RequireString(request, "cluster_name") if err != nil { - return mcp.NewToolResultError("Missing required parameter 'cluster_name'. " + + return adapter.NewErrorResult("Missing required parameter 'cluster_name'. " + "Please provide the name of the cluster for peer clusters operation."), nil } @@ -291,15 +291,15 @@ func (b *PulsarAdminClusterToolBuilder) handlePeerClustersResource(client cmduti case "update": return b.updatePeerClusters(client, clusterName, request) default: - return mcp.NewToolResultError(fmt.Sprintf("Unsupported peer_clusters operation: %s", operation)), nil + return adapter.NewErrorResult("Unsupported peer_clusters operation: %s", operation), nil } } // Handle failure domain resource operations -func (b *PulsarAdminClusterToolBuilder) handleFailureDomainResource(client cmdutils.Client, operation string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - clusterName, err := request.RequireString("cluster_name") +func (b *PulsarAdminClusterToolBuilder) handleFailureDomainResource(client cmdutils.Client, operation string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + clusterName, err := adapter.RequireString(request, "cluster_name") if err != nil { - return mcp.NewToolResultError("Missing required parameter 'cluster_name'. " + + return adapter.NewErrorResult("Missing required parameter 'cluster_name'. " + "Please provide the name of the cluster for failure domain operation."), nil } @@ -307,9 +307,9 @@ func (b *PulsarAdminClusterToolBuilder) handleFailureDomainResource(client cmdut case "list": return b.listFailureDomains(client, clusterName) case "get": - domainName, err := request.RequireString("domain_name") + domainName, err := adapter.RequireString(request, "domain_name") if err != nil { - return mcp.NewToolResultError("Missing required parameter 'domain_name'. " + + return adapter.NewErrorResult("Missing required parameter 'domain_name'. " + "Please provide the name of the failure domain."), nil } return b.getFailureDomain(client, clusterName, domainName) @@ -318,18 +318,18 @@ func (b *PulsarAdminClusterToolBuilder) handleFailureDomainResource(client cmdut case "update": return b.updateFailureDomain(client, clusterName, request) case "delete": - domainName, err := request.RequireString("domain_name") + domainName, err := adapter.RequireString(request, "domain_name") if err != nil { - return mcp.NewToolResultError("Missing required parameter 'domain_name'. " + + return adapter.NewErrorResult("Missing required parameter 'domain_name'. " + "Please provide the name of the failure domain to delete."), nil } return b.deleteFailureDomain(client, clusterName, domainName) default: - return mcp.NewToolResultError(fmt.Sprintf("Unsupported failure_domain operation: %s", operation)), nil + return adapter.NewErrorResult("Unsupported failure_domain operation: %s", operation), nil } } -func (b *PulsarAdminClusterToolBuilder) handleClusterList(client cmdutils.Client) (*mcp.CallToolResult, error) { +func (b *PulsarAdminClusterToolBuilder) handleClusterList(client cmdutils.Client) (*mcpsdk.CallToolResult, error) { // Get cluster list clusters, err := client.Clusters().List() if err != nil { @@ -339,7 +339,7 @@ func (b *PulsarAdminClusterToolBuilder) handleClusterList(client cmdutils.Client return b.marshalResponse(clusters) } -func (b *PulsarAdminClusterToolBuilder) getClusterData(client cmdutils.Client, clusterName string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminClusterToolBuilder) getClusterData(client cmdutils.Client, clusterName string) (*mcpsdk.CallToolResult, error) { // Get cluster data clusterData, err := client.Clusters().Get(clusterName) if err != nil { @@ -349,10 +349,10 @@ func (b *PulsarAdminClusterToolBuilder) getClusterData(client cmdutils.Client, c return b.marshalResponse(clusterData) } -func (b *PulsarAdminClusterToolBuilder) createCluster(client cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - clusterName, err := request.RequireString("cluster_name") +func (b *PulsarAdminClusterToolBuilder) createCluster(client cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + clusterName, err := adapter.RequireString(request, "cluster_name") if err != nil { - return mcp.NewToolResultError("Missing required parameter 'cluster_name'. " + + return adapter.NewErrorResult("Missing required parameter 'cluster_name'. " + "Please provide the name of the cluster to create."), nil } @@ -362,19 +362,19 @@ func (b *PulsarAdminClusterToolBuilder) createCluster(client cmdutils.Client, re } // Set optional parameters if provided - if serviceURL := request.GetString("service_url", ""); serviceURL != "" { + if serviceURL := adapter.GetString(request, "service_url", ""); serviceURL != "" { clusterData.ServiceURL = serviceURL } - if serviceURLTls := request.GetString("service_url_tls", ""); serviceURLTls != "" { + if serviceURLTls := adapter.GetString(request, "service_url_tls", ""); serviceURLTls != "" { clusterData.ServiceURLTls = serviceURLTls } - if brokerServiceURL := request.GetString("broker_service_url", ""); brokerServiceURL != "" { + if brokerServiceURL := adapter.GetString(request, "broker_service_url", ""); brokerServiceURL != "" { clusterData.BrokerServiceURL = brokerServiceURL } - if brokerServiceURLTls := request.GetString("broker_service_url_tls", ""); brokerServiceURLTls != "" { + if brokerServiceURLTls := adapter.GetString(request, "broker_service_url_tls", ""); brokerServiceURLTls != "" { clusterData.BrokerServiceURLTls = brokerServiceURLTls } - if peerClusters := request.GetStringSlice("peer_cluster_names", []string{}); len(peerClusters) > 0 { + if peerClusters := adapter.GetStringSlice(request, "peer_cluster_names", []string{}); len(peerClusters) > 0 { clusterData.PeerClusterNames = peerClusters } @@ -384,13 +384,13 @@ func (b *PulsarAdminClusterToolBuilder) createCluster(client cmdutils.Client, re return b.handleError("create cluster", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Cluster %s created successfully", clusterName)), nil + return adapter.NewTextResult(fmt.Sprintf("Cluster %s created successfully", clusterName)), nil } -func (b *PulsarAdminClusterToolBuilder) updateCluster(client cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - clusterName, err := request.RequireString("cluster_name") +func (b *PulsarAdminClusterToolBuilder) updateCluster(client cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + clusterName, err := adapter.RequireString(request, "cluster_name") if err != nil { - return mcp.NewToolResultError("Missing required parameter 'cluster_name'. " + + return adapter.NewErrorResult("Missing required parameter 'cluster_name'. " + "Please provide the name of the cluster to update."), nil } @@ -400,19 +400,19 @@ func (b *PulsarAdminClusterToolBuilder) updateCluster(client cmdutils.Client, re } // Set optional parameters if provided - if serviceURL := request.GetString("service_url", ""); serviceURL != "" { + if serviceURL := adapter.GetString(request, "service_url", ""); serviceURL != "" { clusterData.ServiceURL = serviceURL } - if serviceURLTls := request.GetString("service_url_tls", ""); serviceURLTls != "" { + if serviceURLTls := adapter.GetString(request, "service_url_tls", ""); serviceURLTls != "" { clusterData.ServiceURLTls = serviceURLTls } - if brokerServiceURL := request.GetString("broker_service_url", ""); brokerServiceURL != "" { + if brokerServiceURL := adapter.GetString(request, "broker_service_url", ""); brokerServiceURL != "" { clusterData.BrokerServiceURL = brokerServiceURL } - if brokerServiceURLTls := request.GetString("broker_service_url_tls", ""); brokerServiceURLTls != "" { + if brokerServiceURLTls := adapter.GetString(request, "broker_service_url_tls", ""); brokerServiceURLTls != "" { clusterData.BrokerServiceURLTls = brokerServiceURLTls } - if peerClusters := request.GetStringSlice("peer_cluster_names", []string{}); len(peerClusters) > 0 { + if peerClusters := adapter.GetStringSlice(request, "peer_cluster_names", []string{}); len(peerClusters) > 0 { clusterData.PeerClusterNames = peerClusters } @@ -422,20 +422,20 @@ func (b *PulsarAdminClusterToolBuilder) updateCluster(client cmdutils.Client, re return b.handleError("update cluster", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Cluster %s updated successfully", clusterName)), nil + return adapter.NewTextResult(fmt.Sprintf("Cluster %s updated successfully", clusterName)), nil } -func (b *PulsarAdminClusterToolBuilder) deleteCluster(client cmdutils.Client, clusterName string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminClusterToolBuilder) deleteCluster(client cmdutils.Client, clusterName string) (*mcpsdk.CallToolResult, error) { // Delete cluster err := client.Clusters().Delete(clusterName) if err != nil { return b.handleError("delete cluster", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Cluster %s deleted successfully", clusterName)), nil + return adapter.NewTextResult(fmt.Sprintf("Cluster %s deleted successfully", clusterName)), nil } -func (b *PulsarAdminClusterToolBuilder) getPeerClusters(client cmdutils.Client, clusterName string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminClusterToolBuilder) getPeerClusters(client cmdutils.Client, clusterName string) (*mcpsdk.CallToolResult, error) { // Get peer clusters peerClusters, err := client.Clusters().GetPeerClusters(clusterName) if err != nil { @@ -445,10 +445,10 @@ func (b *PulsarAdminClusterToolBuilder) getPeerClusters(client cmdutils.Client, return b.marshalResponse(peerClusters) } -func (b *PulsarAdminClusterToolBuilder) updatePeerClusters(client cmdutils.Client, clusterName string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - peerClusters, err := request.RequireStringSlice("peer_cluster_names") +func (b *PulsarAdminClusterToolBuilder) updatePeerClusters(client cmdutils.Client, clusterName string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + peerClusters, err := adapter.RequireStringSlice(request, "peer_cluster_names") if err != nil { - return mcp.NewToolResultError("Missing required parameter 'peer_cluster_names'. " + + return adapter.NewErrorResult("Missing required parameter 'peer_cluster_names'. " + "Please provide an array of peer cluster names to set."), nil } @@ -458,10 +458,10 @@ func (b *PulsarAdminClusterToolBuilder) updatePeerClusters(client cmdutils.Clien return b.handleError("update peer clusters", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Peer clusters for %s updated successfully", clusterName)), nil + return adapter.NewTextResult(fmt.Sprintf("Peer clusters for %s updated successfully", clusterName)), nil } -func (b *PulsarAdminClusterToolBuilder) listFailureDomains(client cmdutils.Client, clusterName string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminClusterToolBuilder) listFailureDomains(client cmdutils.Client, clusterName string) (*mcpsdk.CallToolResult, error) { // Get failure domains list failureDomains, err := client.Clusters().ListFailureDomains(clusterName) if err != nil { @@ -471,7 +471,7 @@ func (b *PulsarAdminClusterToolBuilder) listFailureDomains(client cmdutils.Clien return b.marshalResponse(failureDomains) } -func (b *PulsarAdminClusterToolBuilder) getFailureDomain(client cmdutils.Client, clusterName, domainName string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminClusterToolBuilder) getFailureDomain(client cmdutils.Client, clusterName, domainName string) (*mcpsdk.CallToolResult, error) { // Get failure domain failureDomain, err := client.Clusters().GetFailureDomain(clusterName, domainName) if err != nil { @@ -481,16 +481,16 @@ func (b *PulsarAdminClusterToolBuilder) getFailureDomain(client cmdutils.Client, return b.marshalResponse(failureDomain) } -func (b *PulsarAdminClusterToolBuilder) createFailureDomain(client cmdutils.Client, clusterName string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - domainName, err := request.RequireString("domain_name") +func (b *PulsarAdminClusterToolBuilder) createFailureDomain(client cmdutils.Client, clusterName string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + domainName, err := adapter.RequireString(request, "domain_name") if err != nil { - return mcp.NewToolResultError("Missing required parameter 'domain_name'. " + + return adapter.NewErrorResult("Missing required parameter 'domain_name'. " + "Please provide the name of the failure domain to create."), nil } - brokers, err := request.RequireStringSlice("brokers") + brokers, err := adapter.RequireStringSlice(request, "brokers") if err != nil { - return mcp.NewToolResultError("Missing required parameter 'brokers'. " + + return adapter.NewErrorResult("Missing required parameter 'brokers'. " + "Please provide an array of broker names to include in this failure domain."), nil } @@ -507,19 +507,19 @@ func (b *PulsarAdminClusterToolBuilder) createFailureDomain(client cmdutils.Clie return b.handleError("create failure domain", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Failure domain %s created successfully in cluster %s", domainName, clusterName)), nil + return adapter.NewTextResult(fmt.Sprintf("Failure domain %s created successfully in cluster %s", domainName, clusterName)), nil } -func (b *PulsarAdminClusterToolBuilder) updateFailureDomain(client cmdutils.Client, clusterName string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - domainName, err := request.RequireString("domain_name") +func (b *PulsarAdminClusterToolBuilder) updateFailureDomain(client cmdutils.Client, clusterName string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + domainName, err := adapter.RequireString(request, "domain_name") if err != nil { - return mcp.NewToolResultError("Missing required parameter 'domain_name'. " + + return adapter.NewErrorResult("Missing required parameter 'domain_name'. " + "Please provide the name of the failure domain to update."), nil } - brokers, err := request.RequireStringSlice("brokers") + brokers, err := adapter.RequireStringSlice(request, "brokers") if err != nil { - return mcp.NewToolResultError("Missing required parameter 'brokers'. " + + return adapter.NewErrorResult("Missing required parameter 'brokers'. " + "Please provide an array of broker names to include in this failure domain."), nil } @@ -536,11 +536,11 @@ func (b *PulsarAdminClusterToolBuilder) updateFailureDomain(client cmdutils.Clie return b.handleError("update failure domain", err), nil } - return mcp.NewToolResultText( + return adapter.NewTextResult( fmt.Sprintf("Failure domain %s updated successfully in cluster %s", domainName, clusterName)), nil } -func (b *PulsarAdminClusterToolBuilder) deleteFailureDomain(client cmdutils.Client, clusterName, domainName string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminClusterToolBuilder) deleteFailureDomain(client cmdutils.Client, clusterName, domainName string) (*mcpsdk.CallToolResult, error) { // Create failure domain data for deletion failureDomainData := utils.FailureDomainData{ ClusterName: clusterName, @@ -553,6 +553,6 @@ func (b *PulsarAdminClusterToolBuilder) deleteFailureDomain(client cmdutils.Clie return b.handleError("delete failure domain", err), nil } - return mcp.NewToolResultText( + return adapter.NewTextResult( fmt.Sprintf("Failure domain %s deleted successfully from cluster %s", domainName, clusterName)), nil } diff --git a/pkg/mcp/builders/pulsar/consume.go b/pkg/mcp/builders/pulsar/consume.go index 52d7763..053303d 100644 --- a/pkg/mcp/builders/pulsar/consume.go +++ b/pkg/mcp/builders/pulsar/consume.go @@ -17,14 +17,13 @@ package pulsar import ( "context" "encoding/json" - "fmt" "strings" "time" "github.com/apache/pulsar-client-go/pulsar" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" mcpCtx "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" ) @@ -58,7 +57,7 @@ func NewPulsarClientConsumeToolBuilder() *PulsarClientConsumeToolBuilder { // BuildTools builds the Pulsar Client Consumer tool list // This is the core method implementing the ToolBuilder interface -func (b *PulsarClientConsumeToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]server.ServerTool, error) { +func (b *PulsarClientConsumeToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]builders.ServerTool, error) { // Check features - return empty list if no required features are present if !b.HasAnyRequiredFeature(config.Features) { return nil, nil @@ -73,7 +72,7 @@ func (b *PulsarClientConsumeToolBuilder) BuildTools(_ context.Context, config bu tool := b.buildConsumeTool() handler := b.buildConsumeHandler(config.ReadOnly) - return []server.ServerTool{ + return []builders.ServerTool{ { Tool: tool, Handler: handler, @@ -83,7 +82,7 @@ func (b *PulsarClientConsumeToolBuilder) BuildTools(_ context.Context, config bu // buildConsumeTool builds the Pulsar Client Consumer MCP tool definition // Migrated from the original tool definition logic -func (b *PulsarClientConsumeToolBuilder) buildConsumeTool() mcp.Tool { +func (b *PulsarClientConsumeToolBuilder) buildConsumeTool() *mcpsdk.Tool { toolDesc := "Consume messages from a Pulsar topic. " + "This tool allows you to consume messages from a specified Pulsar topic with various options " + "to control the subscription behavior, message processing, and display format. " + @@ -93,49 +92,49 @@ func (b *PulsarClientConsumeToolBuilder) buildConsumeTool() mcp.Tool { "timeout settings, and message display options. " + "Do not use this tool for Kafka protocol operations. Use 'kafka_client_consume' instead." - return mcp.NewTool("pulsar_client_consume", - mcp.WithDescription(toolDesc), - mcp.WithString("topic", mcp.Required(), - mcp.Description("The fully qualified topic name to consume from (format: [persistent|non-persistent]://tenant/namespace/topic). "+ + return builders.NewTool("pulsar_client_consume", + builders.WithDescription(toolDesc), + builders.WithString("topic", builders.Required(), + builders.Description("The fully qualified topic name to consume from (format: [persistent|non-persistent]://tenant/namespace/topic). "+ "For partitioned topics, you can consume from all partitions by specifying the base topic name "+ "or from a specific partition by appending -partition-N to the topic name."), ), - mcp.WithString("subscription-name", mcp.Required(), - mcp.Description("The subscription name for this consumer. "+ + builders.WithString("subscription-name", builders.Required(), + builders.Description("The subscription name for this consumer. "+ "A subscription represents a named cursor for tracking message consumption progress. "+ "Multiple consumers can share the same subscription name to form a consumer group."), ), - mcp.WithString("subscription-type", - mcp.Description("Subscription type controlling message distribution among consumers:\\n"+ + builders.WithString("subscription-type", + builders.Description("Subscription type controlling message distribution among consumers:\\n"+ "- exclusive: Only one consumer can consume from the subscription at a time\\n"+ "- shared: Messages are distributed across all consumers in a round-robin fashion\\n"+ "- failover: Only one active consumer, others act as backups\\n"+ "- key_shared: Messages with the same key are delivered to the same consumer (default: exclusive)"), ), - mcp.WithString("subscription-mode", - mcp.Description("Subscription durability mode:\\n"+ + builders.WithString("subscription-mode", + builders.Description("Subscription durability mode:\\n"+ "- durable: Subscription persists even when all consumers disconnect\\n"+ "- non-durable: Subscription is deleted when all consumers disconnect (default: durable)"), ), - mcp.WithString("initial-position", - mcp.Description("Initial cursor position for new subscriptions:\\n"+ + builders.WithString("initial-position", + builders.Description("Initial cursor position for new subscriptions:\\n"+ "- latest: Start consuming from the latest (most recent) message\\n"+ "- earliest: Start consuming from the earliest (oldest available) message (default: latest)"), ), - mcp.WithNumber("num-messages", - mcp.Description("Maximum number of messages to consume in this session. "+ + builders.WithNumber("num-messages", + builders.Description("Maximum number of messages to consume in this session. "+ "Set to 0 for unlimited consumption until timeout. (default: 10)"), ), - mcp.WithNumber("timeout", - mcp.Description("Maximum time to wait for messages in seconds. "+ + builders.WithNumber("timeout", + builders.Description("Maximum time to wait for messages in seconds. "+ "The consumer will stop after this timeout even if fewer messages were received. (default: 30)"), ), - mcp.WithBoolean("show-properties", - mcp.Description("Include message properties in the output. "+ + builders.WithBoolean("show-properties", + builders.Description("Include message properties in the output. "+ "Message properties are key-value pairs attached to messages for metadata purposes. (default: false)"), ), - mcp.WithBoolean("hide-payload", - mcp.Description("Exclude message payload from the output. "+ + builders.WithBoolean("hide-payload", + builders.Description("Exclude message payload from the output. "+ "Useful when you only need message metadata or are dealing with large payloads. (default: false)"), ), ) @@ -143,38 +142,38 @@ func (b *PulsarClientConsumeToolBuilder) buildConsumeTool() mcp.Tool { // buildConsumeHandler builds the Pulsar Client Consumer handler function // Migrated from the original handler logic -func (b *PulsarClientConsumeToolBuilder) buildConsumeHandler(_ bool) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarClientConsumeToolBuilder) buildConsumeHandler(_ bool) func(context.Context, *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Extract required parameters with validation - topic, err := request.RequireString("topic") + topic, err := adapter.RequireString(request, "topic") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get topic: %v", err)), nil + return adapter.NewErrorResult("Failed to get topic: %v", err), nil } - subscriptionName, err := request.RequireString("subscription-name") + subscriptionName, err := adapter.RequireString(request, "subscription-name") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get subscription name: %v", err)), nil + return adapter.NewErrorResult("Failed to get subscription name: %v", err), nil } // Set default values and extract optional parameters - subscriptionType := request.GetString("subscription-type", "exclusive") - subscriptionMode := request.GetString("subscription-mode", "durable") - initialPosition := request.GetString("initial-position", "latest") - numMessages := int(request.GetFloat("num-messages", 10)) - timeout := int(request.GetFloat("timeout", 30)) - showProperties := request.GetBool("show-properties", false) - hidePayload := request.GetBool("hide-payload", false) + subscriptionType := adapter.GetString(request, "subscription-type", "exclusive") + subscriptionMode := adapter.GetString(request, "subscription-mode", "durable") + initialPosition := adapter.GetString(request, "initial-position", "latest") + numMessages := int(adapter.GetFloat(request, "num-messages", 10)) + timeout := int(adapter.GetFloat(request, "timeout", 30)) + showProperties := adapter.GetBool(request, "show-properties", false) + hidePayload := adapter.GetBool(request, "hide-payload", false) // Get Pulsar session from context session := mcpCtx.GetPulsarSession(ctx) if session == nil { - return mcp.NewToolResultError("Pulsar session not found in context"), nil + return adapter.NewErrorResult("Pulsar session not found in context"), nil } // Setup client client, err := session.GetPulsarClient() if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to create Pulsar client: %v", err)), nil + return adapter.NewErrorResult("Failed to create Pulsar client: %v", err), nil } defer client.Close() @@ -196,7 +195,7 @@ func (b *PulsarClientConsumeToolBuilder) buildConsumeHandler(_ bool) func(contex case "key_shared": consumerOpts.Type = pulsar.KeyShared default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid subscription type: %s. Valid types: exclusive, shared, failover, key_shared", subscriptionType)), nil + return adapter.NewErrorResult("Invalid subscription type: %s. Valid types: exclusive, shared, failover, key_shared", subscriptionType), nil } // Set subscription mode @@ -206,7 +205,7 @@ func (b *PulsarClientConsumeToolBuilder) buildConsumeHandler(_ bool) func(contex case "non-durable": consumerOpts.SubscriptionMode = pulsar.NonDurable default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid subscription mode: %s. Valid modes: durable, non-durable", subscriptionMode)), nil + return adapter.NewErrorResult("Invalid subscription mode: %s. Valid modes: durable, non-durable", subscriptionMode), nil } // Set initial position @@ -216,13 +215,13 @@ func (b *PulsarClientConsumeToolBuilder) buildConsumeHandler(_ bool) func(contex case "earliest": consumerOpts.SubscriptionInitialPosition = pulsar.SubscriptionPositionEarliest default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid initial position: %s. Valid positions: latest, earliest", initialPosition)), nil + return adapter.NewErrorResult("Invalid initial position: %s. Valid positions: latest, earliest", initialPosition), nil } // Create consumer consumer, err := client.Subscribe(consumerOpts) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to create consumer: %v", err)), nil + return adapter.NewErrorResult("Failed to create consumer: %v", err), nil } defer consumer.Close() @@ -257,7 +256,7 @@ func (b *PulsarClientConsumeToolBuilder) buildConsumeHandler(_ bool) func(contex if err == context.DeadlineExceeded || err == context.Canceled { break } - return mcp.NewToolResultError(fmt.Sprintf("Error receiving message: %v", err)), nil + return adapter.NewErrorResult("Error receiving message: %v", err), nil } // Process the message @@ -306,15 +305,15 @@ func (b *PulsarClientConsumeToolBuilder) buildConsumeHandler(_ bool) func(contex // Unified error handling and utility functions // handleError provides unified error handling -func (b *PulsarClientConsumeToolBuilder) handleError(operation string, err error) *mcp.CallToolResult { - return mcp.NewToolResultError(fmt.Sprintf("Failed to %s: %v", operation, err)) +func (b *PulsarClientConsumeToolBuilder) handleError(operation string, err error) *mcpsdk.CallToolResult { + return adapter.NewErrorResult("Failed to %s: %v", operation, err) } // marshalResponse provides unified JSON serialization for responses -func (b *PulsarClientConsumeToolBuilder) marshalResponse(data interface{}) (*mcp.CallToolResult, error) { +func (b *PulsarClientConsumeToolBuilder) marshalResponse(data interface{}) (*mcpsdk.CallToolResult, error) { jsonBytes, err := json.Marshal(data) if err != nil { return b.handleError("marshal response", err), nil } - return mcp.NewToolResultText(string(jsonBytes)), nil + return adapter.NewTextResult(string(jsonBytes)), nil } diff --git a/pkg/mcp/builders/pulsar/functions.go b/pkg/mcp/builders/pulsar/functions.go index 6bf8789..adff612 100644 --- a/pkg/mcp/builders/pulsar/functions.go +++ b/pkg/mcp/builders/pulsar/functions.go @@ -20,10 +20,10 @@ import ( "fmt" "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/pulsarctl/pkg/cmdutils" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" mcpCtx "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" ) @@ -58,7 +58,7 @@ func NewPulsarAdminFunctionsToolBuilder() *PulsarAdminFunctionsToolBuilder { // BuildTools builds the Pulsar admin functions tool list // This is the core method implementing the ToolBuilder interface -func (b *PulsarAdminFunctionsToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]server.ServerTool, error) { +func (b *PulsarAdminFunctionsToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]builders.ServerTool, error) { // Check features - return empty list if no required features are present if !b.HasAnyRequiredFeature(config.Features) { return nil, nil @@ -73,7 +73,7 @@ func (b *PulsarAdminFunctionsToolBuilder) BuildTools(_ context.Context, config b tool := b.buildPulsarAdminFunctionsTool() handler := b.buildPulsarAdminFunctionsHandler(config.ReadOnly) - return []server.ServerTool{ + return []builders.ServerTool{ { Tool: tool, Handler: handler, @@ -83,7 +83,7 @@ func (b *PulsarAdminFunctionsToolBuilder) BuildTools(_ context.Context, config b // buildPulsarAdminFunctionsTool builds the Pulsar admin functions MCP tool definition // Migrated from the original tool definition logic -func (b *PulsarAdminFunctionsToolBuilder) buildPulsarAdminFunctionsTool() mcp.Tool { +func (b *PulsarAdminFunctionsToolBuilder) buildPulsarAdminFunctionsTool() *mcpsdk.Tool { toolDesc := "Manage Apache Pulsar Functions for stream processing. " + "Pulsar Functions are lightweight compute processes that can consume messages from one or more Pulsar topics, " + "apply user-defined processing logic, and produce results to another topic. " + @@ -109,95 +109,95 @@ func (b *PulsarAdminFunctionsToolBuilder) buildPulsarAdminFunctionsTool() mcp.To "- putstate: Store state in a function's state store\n" + "- trigger: Manually trigger a function with a specific value" - return mcp.NewTool("pulsar_admin_functions", - mcp.WithDescription(toolDesc), - mcp.WithString("operation", mcp.Required(), - mcp.Description(operationDesc)), - mcp.WithString("tenant", mcp.Required(), - mcp.Description("The tenant name. Tenants are the primary organizational unit in Pulsar, "+ + return builders.NewTool("pulsar_admin_functions", + builders.WithDescription(toolDesc), + builders.WithString("operation", builders.Required(), + builders.Description(operationDesc)), + builders.WithString("tenant", builders.Required(), + builders.Description("The tenant name. Tenants are the primary organizational unit in Pulsar, "+ "providing multi-tenancy and resource isolation. Functions deployed within a tenant "+ "inherit its permissions and resource quotas.")), - mcp.WithString("namespace", mcp.Required(), - mcp.Description("The namespace name. Namespaces are logical groupings of topics and functions "+ + builders.WithString("namespace", builders.Required(), + builders.Description("The namespace name. Namespaces are logical groupings of topics and functions "+ "within a tenant. They encapsulate configuration policies and access control. "+ "Functions in a namespace typically process topics within the same namespace.")), - mcp.WithString("name", - mcp.Description("The function name. Required for all operations except 'list'. "+ + builders.WithString("name", + builders.Description("The function name. Required for all operations except 'list'. "+ "Names should be descriptive of the function's purpose and must be unique within a namespace. "+ "Function names are used in metrics, logs, and when addressing the function via APIs.")), // Additional parameters for specific operations - mcp.WithString("classname", - mcp.Description("The fully qualified class name implementing the function. Required for 'create' operation, optional for 'update'. "+ + builders.WithString("classname", + builders.Description("The fully qualified class name implementing the function. Required for 'create' operation, optional for 'update'. "+ "For Java functions, this should be the class that implements pulsar function interfaces. "+ "For Python, this MUST be in format of `.` - for example: "+ "if file is '/path/to/exclamation.py' with class 'ExclamationFunction', classname must be 'exclamation.ExclamationFunction'; "+ "if file is '/path/to/double_number.py' with class 'DoubleNumber', classname must be 'double_number.DoubleNumber'. "+ "Common error: using just the class name 'DoubleNumber' (without filename prefix) will cause function creation to fail. "+ "Go functions should specify the 'main' function of the binary.")), - mcp.WithArray("inputs", - mcp.Description("The input topics for the function (array of strings). Optional for 'create' and 'update' operations. "+ + builders.WithArray("inputs", + builders.Description("The input topics for the function (array of strings). Optional for 'create' and 'update' operations. "+ "Topics must be specified in the format 'persistent://tenant/namespace/topic'. "+ "Functions can consume from multiple topics, each with potentially different serialization types. "+ "All input topics should exist before the function is created."), - mcp.Items( + builders.Items( map[string]interface{}{ "type": "string", "description": "input topic", }, ), ), - mcp.WithString("output", - mcp.Description("The output topic for the function results. Optional for 'create' and 'update' operations. "+ + builders.WithString("output", + builders.Description("The output topic for the function results. Optional for 'create' and 'update' operations. "+ "Specified in the format 'persistent://tenant/namespace/topic'. "+ "If not set, the function will not produce any output to topics. "+ "The output topic will be automatically created if it doesn't exist.")), - mcp.WithString("jar", - mcp.Description("Path to the JAR file containing the function code. Optional for 'create' and 'update' operations. "+ + builders.WithString("jar", + builders.Description("Path to the JAR file containing the function code. Optional for 'create' and 'update' operations. "+ "Support `file://`, `http://`, `https://`, `function://`, `source://`, `sink://` protocol. "+ "Can be a local path or supported URL protocol accessible to the Pulsar broker. "+ "For Java functions, this should contain all dependencies for the function. "+ "The jar file must be compatible with the Pulsar Functions API.")), - mcp.WithString("py", - mcp.Description("Path to the Python file containing the function code. Optional for 'create' and 'update' operations. "+ + builders.WithString("py", + builders.Description("Path to the Python file containing the function code. Optional for 'create' and 'update' operations. "+ "Support `file://`, `http://`, `https://`, `function://`, `source://`, `sink://` protocol. "+ "Can be a local path or supported URL protocol accessible to the Pulsar broker. "+ "For Python functions, this should be the file path to the Python file, in format of `.py`, `.zip`, or `.whl`. "+ "The Python file must be compatible with the Pulsar Functions API.")), - mcp.WithString("go", - mcp.Description("Path to the Go file containing the function code. Optional for 'create' and 'update' operations. "+ + builders.WithString("go", + builders.Description("Path to the Go file containing the function code. Optional for 'create' and 'update' operations. "+ "Support `file://`, `http://`, `https://`, `function://`, `source://`, `sink://` protocol. "+ "Can be a local path or supported URL protocol accessible to the Pulsar broker. "+ "For Go functions, this should be the file path to the Go file, in format of executable binary. "+ "The Go file must be compatible with the Pulsar Functions API.")), - mcp.WithNumber("parallelism", - mcp.Description("The parallelism factor of the function. Optional for 'create' and 'update' operations. "+ + builders.WithNumber("parallelism", + builders.Description("The parallelism factor of the function. Optional for 'create' and 'update' operations. "+ "Determines how many instances of the function will run concurrently. "+ "Higher values improve throughput but require more resources. "+ "For stateful functions, consider how parallelism affects state consistency. "+ "Default is 1 (single instance).")), - mcp.WithObject("userConfig", - mcp.Description("User-defined config key/values. Optional for 'create' and 'update' operations. "+ + builders.WithObject("userConfig", + builders.Description("User-defined config key/values. Optional for 'create' and 'update' operations. "+ "Provides configuration parameters accessible to the function at runtime. "+ "Specify as a JSON object with string, number, or boolean values. "+ "Common configs include connection parameters, batch sizes, or feature toggles. "+ "Example: {\"maxBatchSize\": 100, \"connectionString\": \"host:port\", \"debugMode\": true}")), - mcp.WithString("key", - mcp.Description("The state key. Required for 'querystate' and 'putstate' operations. "+ + builders.WithString("key", + builders.Description("The state key. Required for 'querystate' and 'putstate' operations. "+ "Keys are used to identify values in the function's state store. "+ "They should be reasonable in length and follow a consistent pattern. "+ "State keys are typically limited to 128 characters.")), - mcp.WithString("value", - mcp.Description("The state value. Required for 'putstate' operation. "+ + builders.WithString("value", + builders.Description("The state value. Required for 'putstate' operation. "+ "Values are stored in the function's state system. "+ "For simple values, specify as a string. For complex objects, use JSON-serialized strings. "+ "State values are typically limited to 1MB in size.")), - mcp.WithString("topic", - mcp.Description("The specific topic name that the function should consume from. Optional for 'trigger' operation. "+ + builders.WithString("topic", + builders.Description("The specific topic name that the function should consume from. Optional for 'trigger' operation. "+ "Specified in the format 'persistent://tenant/namespace/topic'. "+ "Used when triggering a function that consumes from multiple topics. "+ "If not provided, the first input topic will be used.")), - mcp.WithString("triggerValue", - mcp.Description("The value with which to trigger the function. Required for 'trigger' operation. "+ + builders.WithString("triggerValue", + builders.Description("The value with which to trigger the function. Required for 'trigger' operation. "+ "This value will be passed to the function as if it were a message from the input topic. "+ "String values are sent as is; for typed values, ensure proper formatting based on function expectations. "+ "The function processes this value just like a normal message.")), @@ -206,21 +206,21 @@ func (b *PulsarAdminFunctionsToolBuilder) buildPulsarAdminFunctionsTool() mcp.To // buildPulsarAdminFunctionsHandler builds the Pulsar admin functions handler function // Migrated from the original handler logic -func (b *PulsarAdminFunctionsToolBuilder) buildPulsarAdminFunctionsHandler(readOnly bool) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminFunctionsToolBuilder) buildPulsarAdminFunctionsHandler(readOnly bool) func(context.Context, *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get Pulsar session from context session := mcpCtx.GetPulsarSession(ctx) if session == nil { - return mcp.NewToolResultError("Pulsar session not found in context"), nil + return adapter.NewErrorResult("Pulsar session not found in context"), nil } client, err := session.GetAdminV3Client() if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get Pulsar client: %v", err)), nil + return adapter.NewErrorResult("Failed to get Pulsar client: %v", err), nil } // Extract and validate operation parameter - operation, err := request.RequireString("operation") + operation, err := adapter.RequireString(request, "operation") if err != nil { return b.handleError("get operation", err), nil } @@ -247,12 +247,12 @@ func (b *PulsarAdminFunctionsToolBuilder) buildPulsarAdminFunctionsHandler(readO } // Extract common parameters - tenant, err := request.RequireString("tenant") + tenant, err := adapter.RequireString(request, "tenant") if err != nil { return b.handleError("get tenant", fmt.Errorf("missing required parameter 'tenant': %v. A tenant is required for all Pulsar Functions operations", err)), nil } - namespace, err := request.RequireString("namespace") + namespace, err := adapter.RequireString(request, "namespace") if err != nil { return b.handleError("get namespace", fmt.Errorf("missing required parameter 'namespace': %v. A namespace is required for all Pulsar Functions operations", err)), nil } @@ -260,7 +260,7 @@ func (b *PulsarAdminFunctionsToolBuilder) buildPulsarAdminFunctionsHandler(readO // For all operations except 'list', name is required var name string if operation != "list" { - name, err = request.RequireString("name") + name, err = adapter.RequireString(request, "name") if err != nil { return b.handleError("get name", fmt.Errorf("missing required parameter 'name' for operation '%s': %v. The function name must be specified for this operation", operation, err)), nil } @@ -277,7 +277,7 @@ func (b *PulsarAdminFunctionsToolBuilder) buildPulsarAdminFunctionsHandler(readO case "stats": return b.handleFunctionStats(ctx, client, tenant, namespace, name) case "querystate": - key, err := request.RequireString("key") + key, err := adapter.RequireString(request, "key") if err != nil { return b.handleError("get key", fmt.Errorf("missing required parameter 'key' for operation 'querystate': %v. A key is required to look up state in the function's state store", err)), nil } @@ -295,21 +295,21 @@ func (b *PulsarAdminFunctionsToolBuilder) buildPulsarAdminFunctionsHandler(readO case "restart": return b.handleFunctionRestart(ctx, client, tenant, namespace, name) case "putstate": - key, err := request.RequireString("key") + key, err := adapter.RequireString(request, "key") if err != nil { return b.handleError("get key", fmt.Errorf("missing required parameter 'key' for operation 'putstate': %v. A key is required to store state in the function's state store", err)), nil } - value, err := request.RequireString("value") + value, err := adapter.RequireString(request, "value") if err != nil { return b.handleError("get value", fmt.Errorf("missing required parameter 'value' for operation 'putstate': %v. A value is required to store state in the function's state store", err)), nil } return b.handleFunctionPutstate(ctx, client, tenant, namespace, name, key, value) case "trigger": - triggerValue, err := request.RequireString("triggerValue") + triggerValue, err := adapter.RequireString(request, "triggerValue") if err != nil { return b.handleError("get triggerValue", fmt.Errorf("missing required parameter 'triggerValue' for operation 'trigger': %v. A trigger value is required to manually trigger the function", err)), nil } - topic := request.GetString("topic", "") + topic := adapter.GetString(request, "topic", "") return b.handleFunctionTrigger(ctx, client, tenant, namespace, name, triggerValue, topic) default: return b.handleError("handle operation", fmt.Errorf("unsupported operation: %s", operation)), nil @@ -320,7 +320,7 @@ func (b *PulsarAdminFunctionsToolBuilder) buildPulsarAdminFunctionsHandler(readO // Helper functions - delegated operation handlers // handleFunctionList handles the list operation -func (b *PulsarAdminFunctionsToolBuilder) handleFunctionList(_ context.Context, client cmdutils.Client, tenant, namespace string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminFunctionsToolBuilder) handleFunctionList(_ context.Context, client cmdutils.Client, tenant, namespace string) (*mcpsdk.CallToolResult, error) { admin := client.Functions() functions, err := admin.GetFunctions(tenant, namespace) @@ -336,7 +336,7 @@ func (b *PulsarAdminFunctionsToolBuilder) handleFunctionList(_ context.Context, } // handleFunctionGet handles the get operation -func (b *PulsarAdminFunctionsToolBuilder) handleFunctionGet(_ context.Context, client cmdutils.Client, tenant, namespace, name string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminFunctionsToolBuilder) handleFunctionGet(_ context.Context, client cmdutils.Client, tenant, namespace, name string) (*mcpsdk.CallToolResult, error) { admin := client.Functions() functionConfig, err := admin.GetFunction(tenant, namespace, name) @@ -348,7 +348,7 @@ func (b *PulsarAdminFunctionsToolBuilder) handleFunctionGet(_ context.Context, c } // handleFunctionStatus handles the status operation -func (b *PulsarAdminFunctionsToolBuilder) handleFunctionStatus(_ context.Context, client cmdutils.Client, tenant, namespace, name string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminFunctionsToolBuilder) handleFunctionStatus(_ context.Context, client cmdutils.Client, tenant, namespace, name string) (*mcpsdk.CallToolResult, error) { admin := client.Functions() status, err := admin.GetFunctionStatus(tenant, namespace, name) @@ -360,12 +360,12 @@ func (b *PulsarAdminFunctionsToolBuilder) handleFunctionStatus(_ context.Context } // handleFunctionStats handles the stats operation -func (b *PulsarAdminFunctionsToolBuilder) handleFunctionStats(_ context.Context, client cmdutils.Client, tenant, namespace, name string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminFunctionsToolBuilder) handleFunctionStats(_ context.Context, client cmdutils.Client, tenant, namespace, name string) (*mcpsdk.CallToolResult, error) { admin := client.Functions() stats, err := admin.GetFunctionStats(tenant, namespace, name) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get stats for function '%s' in tenant '%s' namespace '%s': %v. Verify the function exists and is running.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to get stats for function '%s' in tenant '%s' namespace '%s': %v. Verify the function exists and is running.", name, tenant, namespace, err)), nil } @@ -373,12 +373,12 @@ func (b *PulsarAdminFunctionsToolBuilder) handleFunctionStats(_ context.Context, } // handleFunctionQuerystate handles the querystate operation -func (b *PulsarAdminFunctionsToolBuilder) handleFunctionQuerystate(_ context.Context, client cmdutils.Client, tenant, namespace, name, key string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminFunctionsToolBuilder) handleFunctionQuerystate(_ context.Context, client cmdutils.Client, tenant, namespace, name, key string) (*mcpsdk.CallToolResult, error) { admin := client.Functions() state, err := admin.GetFunctionState(tenant, namespace, name, key) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to query state for key '%s' in function '%s' (tenant '%s' namespace '%s'): %v. Verify the function exists and has state enabled.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to query state for key '%s' in function '%s' (tenant '%s' namespace '%s'): %v. Verify the function exists and has state enabled.", key, name, tenant, namespace, err)), nil } @@ -394,11 +394,11 @@ func (b *PulsarAdminFunctionsToolBuilder) handleFunctionQuerystate(_ context.Con } // handleFunctionCreate handles the create operation -func (b *PulsarAdminFunctionsToolBuilder) handleFunctionCreate(_ context.Context, client cmdutils.Client, tenant, namespace, name string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminFunctionsToolBuilder) handleFunctionCreate(_ context.Context, client cmdutils.Client, tenant, namespace, name string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Build function configuration from request parameters to validate functionConfig, err := b.buildFunctionConfig(tenant, namespace, name, request, false) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to build function configuration for '%s' in tenant '%s' namespace '%s': %v. Please verify all required parameters are provided correctly.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to build function configuration for '%s' in tenant '%s' namespace '%s': %v. Please verify all required parameters are provided correctly.", name, tenant, namespace, err)), nil } @@ -415,22 +415,22 @@ func (b *PulsarAdminFunctionsToolBuilder) handleFunctionCreate(_ context.Context err = admin.CreateFuncWithURL(functionConfig, packagePath) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to create function '%s' in tenant '%s' namespace '%s': %v. Verify the function configuration is valid.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to create function '%s' in tenant '%s' namespace '%s': %v. Verify the function configuration is valid.", name, tenant, namespace, err)), nil } - return mcp.NewToolResultText(fmt.Sprintf("Created function '%s' successfully in tenant '%s' namespace '%s'. The function configuration has been created.", + return adapter.NewTextResult(fmt.Sprintf("Created function '%s' successfully in tenant '%s' namespace '%s'. The function configuration has been created.", name, tenant, namespace)), nil } // handleFunctionUpdate handles the update operation -func (b *PulsarAdminFunctionsToolBuilder) handleFunctionUpdate(_ context.Context, client cmdutils.Client, tenant, namespace, name string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminFunctionsToolBuilder) handleFunctionUpdate(_ context.Context, client cmdutils.Client, tenant, namespace, name string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { admin := client.Functions() // Build function configuration from request parameters config, err := b.buildFunctionConfig(tenant, namespace, name, request, true) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to build function configuration for '%s' in tenant '%s' namespace '%s': %v. Please verify all parameters are provided correctly.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to build function configuration for '%s' in tenant '%s' namespace '%s': %v. Please verify all parameters are provided correctly.", name, tenant, namespace, err)), nil } @@ -440,72 +440,72 @@ func (b *PulsarAdminFunctionsToolBuilder) handleFunctionUpdate(_ context.Context } err = admin.UpdateFunction(config, "", updateOptions) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to update function '%s' in tenant '%s' namespace '%s': %v. Verify the function exists and the configuration is valid.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to update function '%s' in tenant '%s' namespace '%s': %v. Verify the function exists and the configuration is valid.", name, tenant, namespace, err)), nil } - return mcp.NewToolResultText(fmt.Sprintf("Updated function '%s' successfully in tenant '%s' namespace '%s'. The function configuration has been modified.", + return adapter.NewTextResult(fmt.Sprintf("Updated function '%s' successfully in tenant '%s' namespace '%s'. The function configuration has been modified.", name, tenant, namespace)), nil } // handleFunctionDelete handles the delete operation -func (b *PulsarAdminFunctionsToolBuilder) handleFunctionDelete(_ context.Context, client cmdutils.Client, tenant, namespace, name string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminFunctionsToolBuilder) handleFunctionDelete(_ context.Context, client cmdutils.Client, tenant, namespace, name string) (*mcpsdk.CallToolResult, error) { admin := client.Functions() err := admin.DeleteFunction(tenant, namespace, name) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to delete function '%s' in tenant '%s' namespace '%s': %v. Verify the function exists and you have deletion permissions.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to delete function '%s' in tenant '%s' namespace '%s': %v. Verify the function exists and you have deletion permissions.", name, tenant, namespace, err)), nil } - return mcp.NewToolResultText(fmt.Sprintf("Deleted function '%s' successfully from tenant '%s' namespace '%s'. All running instances have been terminated.", + return adapter.NewTextResult(fmt.Sprintf("Deleted function '%s' successfully from tenant '%s' namespace '%s'. All running instances have been terminated.", name, tenant, namespace)), nil } // handleFunctionStart handles the start operation -func (b *PulsarAdminFunctionsToolBuilder) handleFunctionStart(_ context.Context, client cmdutils.Client, tenant, namespace, name string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminFunctionsToolBuilder) handleFunctionStart(_ context.Context, client cmdutils.Client, tenant, namespace, name string) (*mcpsdk.CallToolResult, error) { admin := client.Functions() err := admin.StartFunction(tenant, namespace, name) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to start function '%s' in tenant '%s' namespace '%s': %v. Verify the function exists and is not already running.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to start function '%s' in tenant '%s' namespace '%s': %v. Verify the function exists and is not already running.", name, tenant, namespace, err)), nil } - return mcp.NewToolResultText(fmt.Sprintf("Started function '%s' successfully in tenant '%s' namespace '%s'. The function instances are now processing messages.", + return adapter.NewTextResult(fmt.Sprintf("Started function '%s' successfully in tenant '%s' namespace '%s'. The function instances are now processing messages.", name, tenant, namespace)), nil } // handleFunctionStop handles the stop operation -func (b *PulsarAdminFunctionsToolBuilder) handleFunctionStop(_ context.Context, client cmdutils.Client, tenant, namespace, name string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminFunctionsToolBuilder) handleFunctionStop(_ context.Context, client cmdutils.Client, tenant, namespace, name string) (*mcpsdk.CallToolResult, error) { admin := client.Functions() err := admin.StopFunction(tenant, namespace, name) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to stop function '%s' in tenant '%s' namespace '%s': %v. Verify the function exists and is currently running.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to stop function '%s' in tenant '%s' namespace '%s': %v. Verify the function exists and is currently running.", name, tenant, namespace, err)), nil } - return mcp.NewToolResultText(fmt.Sprintf("Stopped function '%s' successfully in tenant '%s' namespace '%s'. The function will no longer process messages until restarted.", + return adapter.NewTextResult(fmt.Sprintf("Stopped function '%s' successfully in tenant '%s' namespace '%s'. The function will no longer process messages until restarted.", name, tenant, namespace)), nil } // handleFunctionRestart handles the restart operation -func (b *PulsarAdminFunctionsToolBuilder) handleFunctionRestart(_ context.Context, client cmdutils.Client, tenant, namespace, name string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminFunctionsToolBuilder) handleFunctionRestart(_ context.Context, client cmdutils.Client, tenant, namespace, name string) (*mcpsdk.CallToolResult, error) { admin := client.Functions() err := admin.RestartFunction(tenant, namespace, name) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to restart function '%s' in tenant '%s' namespace '%s': %v. Verify the function exists and is properly deployed.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to restart function '%s' in tenant '%s' namespace '%s': %v. Verify the function exists and is properly deployed.", name, tenant, namespace, err)), nil } - return mcp.NewToolResultText(fmt.Sprintf("Restarted function '%s' successfully in tenant '%s' namespace '%s'. All function instances have been restarted.", + return adapter.NewTextResult(fmt.Sprintf("Restarted function '%s' successfully in tenant '%s' namespace '%s'. All function instances have been restarted.", name, tenant, namespace)), nil } // handleFunctionPutstate handles the putstate operation -func (b *PulsarAdminFunctionsToolBuilder) handleFunctionPutstate(_ context.Context, client cmdutils.Client, tenant, namespace, name, key, value string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminFunctionsToolBuilder) handleFunctionPutstate(_ context.Context, client cmdutils.Client, tenant, namespace, name, key, value string) (*mcpsdk.CallToolResult, error) { admin := client.Functions() err := admin.PutFunctionState(tenant, namespace, name, utils.FunctionState{ @@ -513,16 +513,16 @@ func (b *PulsarAdminFunctionsToolBuilder) handleFunctionPutstate(_ context.Conte StringValue: value, }) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to put state for key '%s' in function '%s' (tenant '%s' namespace '%s'): %v. Verify the function exists and has state enabled.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to put state for key '%s' in function '%s' (tenant '%s' namespace '%s'): %v. Verify the function exists and has state enabled.", key, name, tenant, namespace, err)), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully stored state for key '%s' in function '%s' (tenant '%s' namespace '%s'). State value has been updated.", + return adapter.NewTextResult(fmt.Sprintf("Successfully stored state for key '%s' in function '%s' (tenant '%s' namespace '%s'). State value has been updated.", key, name, tenant, namespace)), nil } // handleFunctionTrigger handles the trigger operation -func (b *PulsarAdminFunctionsToolBuilder) handleFunctionTrigger(_ context.Context, client cmdutils.Client, tenant, namespace, name, triggerValue, topic string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminFunctionsToolBuilder) handleFunctionTrigger(_ context.Context, client cmdutils.Client, tenant, namespace, name, triggerValue, topic string) (*mcpsdk.CallToolResult, error) { admin := client.Functions() var err error @@ -536,7 +536,7 @@ func (b *PulsarAdminFunctionsToolBuilder) handleFunctionTrigger(_ context.Contex } if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to trigger function '%s' in tenant '%s' namespace '%s': %v. Verify the function exists and is running.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to trigger function '%s' in tenant '%s' namespace '%s': %v. Verify the function exists and is running.", name, tenant, namespace, err)), nil } @@ -549,13 +549,13 @@ func (b *PulsarAdminFunctionsToolBuilder) handleFunctionTrigger(_ context.Contex name, tenant, namespace, result) } - return mcp.NewToolResultText(message), nil + return adapter.NewTextResult(message), nil } // Helper functions // buildFunctionConfig builds a Pulsar Function configuration from MCP request parameters -func (b *PulsarAdminFunctionsToolBuilder) buildFunctionConfig(tenant, namespace, name string, request mcp.CallToolRequest, isUpdate bool) (*utils.FunctionConfig, error) { +func (b *PulsarAdminFunctionsToolBuilder) buildFunctionConfig(tenant, namespace, name string, request *mcpsdk.CallToolRequest, isUpdate bool) (*utils.FunctionConfig, error) { config := &utils.FunctionConfig{ Tenant: tenant, Namespace: namespace, @@ -564,20 +564,20 @@ func (b *PulsarAdminFunctionsToolBuilder) buildFunctionConfig(tenant, namespace, // Get required classname parameter (for create operations) if !isUpdate { - classname, err := request.RequireString("classname") + classname, err := adapter.RequireString(request, "classname") if err != nil { return nil, fmt.Errorf("missing required parameter 'classname': %v", err) } config.ClassName = classname } else { // For update, classname is optional - if classname := request.GetString("classname", ""); classname != "" { + if classname := adapter.GetString(request, "classname", ""); classname != "" { config.ClassName = classname } } // Get inputs parameter (array of strings) - args := request.GetArguments() + args, _ := adapter.GetArgumentsMap(request) if inputsInterface, exists := args["inputs"]; exists && inputsInterface != nil { if inputsArray, ok := inputsInterface.([]interface{}); ok { inputSpecs := make(map[string]utils.ConsumerConfig) @@ -596,7 +596,7 @@ func (b *PulsarAdminFunctionsToolBuilder) buildFunctionConfig(tenant, namespace, } // Get optional output parameter - if output := request.GetString("output", ""); output != "" { + if output := adapter.GetString(request, "output", ""); output != "" { config.Output = output } @@ -613,17 +613,17 @@ func (b *PulsarAdminFunctionsToolBuilder) buildFunctionConfig(tenant, namespace, } // Get optional jar parameter - if jar := request.GetString("jar", ""); jar != "" { + if jar := adapter.GetString(request, "jar", ""); jar != "" { config.Jar = &jar } // Get optional py parameter - if py := request.GetString("py", ""); py != "" { + if py := adapter.GetString(request, "py", ""); py != "" { config.Py = &py } // Get optional go parameter - if goFile := request.GetString("go", ""); goFile != "" { + if goFile := adapter.GetString(request, "go", ""); goFile != "" { config.Go = &goFile } @@ -638,15 +638,15 @@ func (b *PulsarAdminFunctionsToolBuilder) buildFunctionConfig(tenant, namespace, } // handleError provides unified error handling -func (b *PulsarAdminFunctionsToolBuilder) handleError(operation string, err error) *mcp.CallToolResult { - return mcp.NewToolResultError(fmt.Sprintf("Failed to %s: %v", operation, err)) +func (b *PulsarAdminFunctionsToolBuilder) handleError(operation string, err error) *mcpsdk.CallToolResult { + return adapter.NewErrorResult("Failed to %s: %v", operation, err) } // marshalResponse provides unified JSON serialization for responses -func (b *PulsarAdminFunctionsToolBuilder) marshalResponse(data interface{}) (*mcp.CallToolResult, error) { +func (b *PulsarAdminFunctionsToolBuilder) marshalResponse(data interface{}) (*mcpsdk.CallToolResult, error) { jsonBytes, err := json.Marshal(data) if err != nil { return b.handleError("marshal response", err), nil } - return mcp.NewToolResultText(string(jsonBytes)), nil + return adapter.NewTextResult(string(jsonBytes)), nil } diff --git a/pkg/mcp/builders/pulsar/functions_worker.go b/pkg/mcp/builders/pulsar/functions_worker.go index d244585..625a860 100644 --- a/pkg/mcp/builders/pulsar/functions_worker.go +++ b/pkg/mcp/builders/pulsar/functions_worker.go @@ -19,10 +19,10 @@ import ( "encoding/json" "fmt" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/pulsarctl/pkg/cmdutils" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" mcpCtx "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" ) @@ -57,7 +57,7 @@ func NewPulsarAdminFunctionsWorkerToolBuilder() *PulsarAdminFunctionsWorkerToolB // BuildTools builds the Pulsar Admin Functions Worker tool list // This is the core method implementing the ToolBuilder interface -func (b *PulsarAdminFunctionsWorkerToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]server.ServerTool, error) { +func (b *PulsarAdminFunctionsWorkerToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]builders.ServerTool, error) { // Check features - return empty list if no required features are present if !b.HasAnyRequiredFeature(config.Features) { return nil, nil @@ -72,7 +72,7 @@ func (b *PulsarAdminFunctionsWorkerToolBuilder) BuildTools(_ context.Context, co tool := b.buildFunctionsWorkerTool() handler := b.buildFunctionsWorkerHandler(config.ReadOnly) - return []server.ServerTool{ + return []builders.ServerTool{ { Tool: tool, Handler: handler, @@ -82,7 +82,7 @@ func (b *PulsarAdminFunctionsWorkerToolBuilder) BuildTools(_ context.Context, co // buildFunctionsWorkerTool builds the Pulsar Admin Functions Worker MCP tool definition // Migrated from the original tool definition logic -func (b *PulsarAdminFunctionsWorkerToolBuilder) buildFunctionsWorkerTool() mcp.Tool { +func (b *PulsarAdminFunctionsWorkerToolBuilder) buildFunctionsWorkerTool() *mcpsdk.Tool { toolDesc := "Unified tool for managing Apache Pulsar Functions Worker resources. " + "Pulsar Functions is a serverless compute framework that allows you to process messages in a streaming fashion. " + "The Functions Worker is the runtime environment that executes and manages Pulsar Functions. " + @@ -99,34 +99,34 @@ func (b *PulsarAdminFunctionsWorkerToolBuilder) buildFunctionsWorkerTool() mcp.T "- cluster_leader: Information about the leader of the functions worker cluster, essential for understanding cluster coordination\\n" + "- function_assignments: Current assignments of functions across the functions worker cluster, showing which functions are running on which workers" - return mcp.NewTool("pulsar_admin_functions_worker", - mcp.WithDescription(toolDesc), - mcp.WithString("resource", mcp.Required(), - mcp.Description(resourceDesc), + return builders.NewTool("pulsar_admin_functions_worker", + builders.WithDescription(toolDesc), + builders.WithString("resource", builders.Required(), + builders.Description(resourceDesc), ), ) } // buildFunctionsWorkerHandler builds the Pulsar Admin Functions Worker handler function // Migrated from the original handler logic -func (b *PulsarAdminFunctionsWorkerToolBuilder) buildFunctionsWorkerHandler(_ bool) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminFunctionsWorkerToolBuilder) buildFunctionsWorkerHandler(_ bool) func(context.Context, *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get Pulsar session from context session := mcpCtx.GetPulsarSession(ctx) if session == nil { - return mcp.NewToolResultError("Pulsar session not found in context"), nil + return adapter.NewErrorResult("Pulsar session not found in context"), nil } // Create the admin client admin, err := session.GetAdminClient() if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get admin client: %v", err)), nil + return adapter.NewErrorResult("Failed to get admin client: %v", err), nil } // Get required resource parameter - resource, err := request.RequireString("resource") + resource, err := adapter.RequireString(request, "resource") if err != nil { - return mcp.NewToolResultError("Missing required parameter 'resource'. " + + return adapter.NewErrorResult("Missing required parameter 'resource'. " + "Please specify one of: function_stats, monitoring_metrics, cluster, cluster_leader, function_assignments"), nil } @@ -143,7 +143,7 @@ func (b *PulsarAdminFunctionsWorkerToolBuilder) buildFunctionsWorkerHandler(_ bo case "function_assignments": return b.handleFunctionsWorkerGetFunctionAssignments(admin) default: - return mcp.NewToolResultError(fmt.Sprintf("Unsupported resource: %s. "+ + return adapter.NewErrorResult(fmt.Sprintf("Unsupported resource: %s. "+ "Please use one of: function_stats, monitoring_metrics, cluster, cluster_leader, function_assignments", resource)), nil } } @@ -152,71 +152,71 @@ func (b *PulsarAdminFunctionsWorkerToolBuilder) buildFunctionsWorkerHandler(_ bo // Unified error handling and utility functions // handleError provides unified error handling -func (b *PulsarAdminFunctionsWorkerToolBuilder) handleError(operation string, err error) *mcp.CallToolResult { - return mcp.NewToolResultError(fmt.Sprintf("Failed to %s: %v", operation, err)) +func (b *PulsarAdminFunctionsWorkerToolBuilder) handleError(operation string, err error) *mcpsdk.CallToolResult { + return adapter.NewErrorResult("Failed to %s: %v", operation, err) } // marshalResponse provides unified JSON serialization for responses -func (b *PulsarAdminFunctionsWorkerToolBuilder) marshalResponse(data interface{}) (*mcp.CallToolResult, error) { +func (b *PulsarAdminFunctionsWorkerToolBuilder) marshalResponse(data interface{}) (*mcpsdk.CallToolResult, error) { jsonBytes, err := json.Marshal(data) if err != nil { return b.handleError("marshal response", err), nil } - return mcp.NewToolResultText(string(jsonBytes)), nil + return adapter.NewTextResult(string(jsonBytes)), nil } // Operation handler functions - migrated from the original implementation // handleFunctionsWorkerFunctionStats handles retrieving function statistics -func (b *PulsarAdminFunctionsWorkerToolBuilder) handleFunctionsWorkerFunctionStats(admin cmdutils.Client) (*mcp.CallToolResult, error) { +func (b *PulsarAdminFunctionsWorkerToolBuilder) handleFunctionsWorkerFunctionStats(admin cmdutils.Client) (*mcpsdk.CallToolResult, error) { // Get function stats stats, err := admin.FunctionsWorker().GetFunctionsStats() if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get functions stats: %v", err)), nil + return adapter.NewErrorResult("Failed to get functions stats: %v", err), nil } return b.marshalResponse(stats) } // handleFunctionsWorkerMonitoringMetrics handles retrieving monitoring metrics -func (b *PulsarAdminFunctionsWorkerToolBuilder) handleFunctionsWorkerMonitoringMetrics(admin cmdutils.Client) (*mcp.CallToolResult, error) { +func (b *PulsarAdminFunctionsWorkerToolBuilder) handleFunctionsWorkerMonitoringMetrics(admin cmdutils.Client) (*mcpsdk.CallToolResult, error) { // Get monitoring metrics metrics, err := admin.FunctionsWorker().GetMetrics() if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get monitoring metrics: %v", err)), nil + return adapter.NewErrorResult("Failed to get monitoring metrics: %v", err), nil } return b.marshalResponse(metrics) } // handleFunctionsWorkerGetCluster handles retrieving cluster information -func (b *PulsarAdminFunctionsWorkerToolBuilder) handleFunctionsWorkerGetCluster(admin cmdutils.Client) (*mcp.CallToolResult, error) { +func (b *PulsarAdminFunctionsWorkerToolBuilder) handleFunctionsWorkerGetCluster(admin cmdutils.Client) (*mcpsdk.CallToolResult, error) { // Get cluster info cluster, err := admin.FunctionsWorker().GetCluster() if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get worker cluster: %v", err)), nil + return adapter.NewErrorResult("Failed to get worker cluster: %v", err), nil } return b.marshalResponse(cluster) } // handleFunctionsWorkerGetClusterLeader handles retrieving cluster leader information -func (b *PulsarAdminFunctionsWorkerToolBuilder) handleFunctionsWorkerGetClusterLeader(admin cmdutils.Client) (*mcp.CallToolResult, error) { +func (b *PulsarAdminFunctionsWorkerToolBuilder) handleFunctionsWorkerGetClusterLeader(admin cmdutils.Client) (*mcpsdk.CallToolResult, error) { // Get cluster leader leader, err := admin.FunctionsWorker().GetClusterLeader() if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get worker cluster leader: %v", err)), nil + return adapter.NewErrorResult("Failed to get worker cluster leader: %v", err), nil } return b.marshalResponse(leader) } // handleFunctionsWorkerGetFunctionAssignments handles retrieving function assignments -func (b *PulsarAdminFunctionsWorkerToolBuilder) handleFunctionsWorkerGetFunctionAssignments(admin cmdutils.Client) (*mcp.CallToolResult, error) { +func (b *PulsarAdminFunctionsWorkerToolBuilder) handleFunctionsWorkerGetFunctionAssignments(admin cmdutils.Client) (*mcpsdk.CallToolResult, error) { // Get function assignments assignments, err := admin.FunctionsWorker().GetAssignments() if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get function assignments: %v", err)), nil + return adapter.NewErrorResult("Failed to get function assignments: %v", err), nil } return b.marshalResponse(assignments) diff --git a/pkg/mcp/builders/pulsar/namespace.go b/pkg/mcp/builders/pulsar/namespace.go index ebef3f4..7573b9b 100644 --- a/pkg/mcp/builders/pulsar/namespace.go +++ b/pkg/mcp/builders/pulsar/namespace.go @@ -21,10 +21,10 @@ import ( "strconv" "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/pulsarctl/pkg/cmdutils" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" mcpCtx "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" ) @@ -59,7 +59,7 @@ func NewPulsarAdminNamespaceToolBuilder() *PulsarAdminNamespaceToolBuilder { // BuildTools builds the Pulsar Admin Namespace tool list // This is the core method implementing the ToolBuilder interface -func (b *PulsarAdminNamespaceToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]server.ServerTool, error) { +func (b *PulsarAdminNamespaceToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]builders.ServerTool, error) { // Check features - return empty list if no required features are present if !b.HasAnyRequiredFeature(config.Features) { return nil, nil @@ -74,7 +74,7 @@ func (b *PulsarAdminNamespaceToolBuilder) BuildTools(_ context.Context, config b tool := b.buildNamespaceTool() handler := b.buildNamespaceHandler(config.ReadOnly) - return []server.ServerTool{ + return []builders.ServerTool{ { Tool: tool, Handler: handler, @@ -84,7 +84,7 @@ func (b *PulsarAdminNamespaceToolBuilder) BuildTools(_ context.Context, config b // buildNamespaceTool builds the Pulsar Admin Namespace MCP tool definition // Migrated from the original tool definition logic -func (b *PulsarAdminNamespaceToolBuilder) buildNamespaceTool() mcp.Tool { +func (b *PulsarAdminNamespaceToolBuilder) buildNamespaceTool() *mcpsdk.Tool { toolDesc := "Manage Pulsar namespaces with various operations. " + "This tool provides functionality to work with namespaces in Apache Pulsar, " + "including listing, creating, deleting, and performing various operations on namespaces." @@ -99,64 +99,64 @@ func (b *PulsarAdminNamespaceToolBuilder) buildNamespaceTool() mcp.Tool { "- unload: Unload a namespace from the current serving broker\n" + "- split_bundle: Split a namespace bundle" - return mcp.NewTool("pulsar_admin_namespace", - mcp.WithDescription(toolDesc), - mcp.WithString("operation", mcp.Required(), - mcp.Description(operationDesc), + return builders.NewTool("pulsar_admin_namespace", + builders.WithDescription(toolDesc), + builders.WithString("operation", builders.Required(), + builders.Description(operationDesc), ), - mcp.WithString("tenant", - mcp.Description("The tenant name. Required for 'list' operation."), + builders.WithString("tenant", + builders.Description("The tenant name. Required for 'list' operation."), ), - mcp.WithString("namespace", - mcp.Description("The namespace name in format 'tenant/namespace'. Required for all operations except 'list'."), + builders.WithString("namespace", + builders.Description("The namespace name in format 'tenant/namespace'. Required for all operations except 'list'."), ), - mcp.WithString("bundles", - mcp.Description("Number of bundles to activate when creating a namespace (default: 0 for default number of bundles). Used with 'create' operation."), + builders.WithString("bundles", + builders.Description("Number of bundles to activate when creating a namespace (default: 0 for default number of bundles). Used with 'create' operation."), ), - mcp.WithArray("clusters", - mcp.Description("List of clusters to assign when creating a namespace. Used with 'create' operation."), - mcp.Items( + builders.WithArray("clusters", + builders.Description("List of clusters to assign when creating a namespace. Used with 'create' operation."), + builders.Items( map[string]interface{}{ "type": "string", "description": "Cluster name", }, ), ), - mcp.WithString("subscription", - mcp.Description("Subscription name. Required for 'unsubscribe' operation, optional for 'clear_backlog'."), + builders.WithString("subscription", + builders.Description("Subscription name. Required for 'unsubscribe' operation, optional for 'clear_backlog'."), ), - mcp.WithString("bundle", - mcp.Description("Bundle name or range. Required for 'split_bundle' operation, optional for 'clear_backlog', 'unsubscribe', and 'unload'."), + builders.WithString("bundle", + builders.Description("Bundle name or range. Required for 'split_bundle' operation, optional for 'clear_backlog', 'unsubscribe', and 'unload'."), ), - mcp.WithString("force", - mcp.Description("Force clear backlog (true/false). Used with 'clear_backlog' operation."), + builders.WithString("force", + builders.Description("Force clear backlog (true/false). Used with 'clear_backlog' operation."), ), - mcp.WithString("unload", - mcp.Description("Unload newly split bundles after splitting (true/false). Used with 'split_bundle' operation."), + builders.WithString("unload", + builders.Description("Unload newly split bundles after splitting (true/false). Used with 'split_bundle' operation."), ), ) } // buildNamespaceHandler builds the Pulsar Admin Namespace handler function // Migrated from the original handler logic -func (b *PulsarAdminNamespaceToolBuilder) buildNamespaceHandler(readOnly bool) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminNamespaceToolBuilder) buildNamespaceHandler(readOnly bool) func(context.Context, *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get operation parameter - operation, err := request.RequireString("operation") + operation, err := adapter.RequireString(request, "operation") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get operation: %v", err)), nil + return adapter.NewErrorResult("Failed to get operation: %v", err), nil } // Get Pulsar session from context session := mcpCtx.GetPulsarSession(ctx) if session == nil { - return mcp.NewToolResultError("Pulsar session not found in context"), nil + return adapter.NewErrorResult("Pulsar session not found in context"), nil } // Create Pulsar client client, err := session.GetAdminClient() if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get admin client: %v", err)), nil + return adapter.NewErrorResult("Failed to get admin client: %v", err), nil } // Route to appropriate handler based on operation @@ -168,7 +168,7 @@ func (b *PulsarAdminNamespaceToolBuilder) buildNamespaceHandler(readOnly bool) f case "create", "delete", "clear_backlog", "unsubscribe", "unload", "split_bundle": // Check if write operations are allowed if readOnly { - return mcp.NewToolResultError(fmt.Sprintf("Operation '%s' not allowed in read-only mode", operation)), nil + return adapter.NewErrorResult("Operation '%s' not allowed in read-only mode", operation), nil } // Route to appropriate write operation handler @@ -187,37 +187,37 @@ func (b *PulsarAdminNamespaceToolBuilder) buildNamespaceHandler(readOnly bool) f return b.handleSplitBundle(ctx, client, request) } default: - return mcp.NewToolResultError(fmt.Sprintf("Unknown operation: %s. Supported operations: list, get_topics, create, delete, clear_backlog, unsubscribe, unload, split_bundle", operation)), nil + return adapter.NewErrorResult("Unknown operation: %s. Supported operations: list, get_topics, create, delete, clear_backlog, unsubscribe, unload, split_bundle", operation), nil } // Should not reach here - return mcp.NewToolResultError("Unexpected error: operation not handled"), nil + return adapter.NewErrorResult("Unexpected error: operation not handled"), nil } } // Unified error handling and utility functions // handleError provides unified error handling -func (b *PulsarAdminNamespaceToolBuilder) handleError(operation string, err error) *mcp.CallToolResult { - return mcp.NewToolResultError(fmt.Sprintf("Failed to %s: %v", operation, err)) +func (b *PulsarAdminNamespaceToolBuilder) handleError(operation string, err error) *mcpsdk.CallToolResult { + return adapter.NewErrorResult("Failed to %s: %v", operation, err) } // marshalResponse provides unified JSON serialization for responses -func (b *PulsarAdminNamespaceToolBuilder) marshalResponse(data interface{}) (*mcp.CallToolResult, error) { +func (b *PulsarAdminNamespaceToolBuilder) marshalResponse(data interface{}) (*mcpsdk.CallToolResult, error) { jsonBytes, err := json.Marshal(data) if err != nil { return b.handleError("marshal response", err), nil } - return mcp.NewToolResultText(string(jsonBytes)), nil + return adapter.NewTextResult(string(jsonBytes)), nil } // Operation handler functions - migrated from the original implementation // handleNamespaceList handles listing namespaces for a tenant -func (b *PulsarAdminNamespaceToolBuilder) handleNamespaceList(_ context.Context, client cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - tenant, err := request.RequireString("tenant") +func (b *PulsarAdminNamespaceToolBuilder) handleNamespaceList(_ context.Context, client cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + tenant, err := adapter.RequireString(request, "tenant") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get tenant name: %v", err)), nil + return adapter.NewErrorResult("Failed to get tenant name: %v", err), nil } // Get namespace list @@ -230,10 +230,10 @@ func (b *PulsarAdminNamespaceToolBuilder) handleNamespaceList(_ context.Context, } // handleNamespaceGetTopics handles getting topics for a namespace -func (b *PulsarAdminNamespaceToolBuilder) handleNamespaceGetTopics(_ context.Context, client cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - namespace, err := request.RequireString("namespace") +func (b *PulsarAdminNamespaceToolBuilder) handleNamespaceGetTopics(_ context.Context, client cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + namespace, err := adapter.RequireString(request, "namespace") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get namespace name: %v", err)), nil + return adapter.NewErrorResult("Failed to get namespace name: %v", err), nil } // Get topics list @@ -246,24 +246,24 @@ func (b *PulsarAdminNamespaceToolBuilder) handleNamespaceGetTopics(_ context.Con } // handleNamespaceCreate handles creating a new namespace -func (b *PulsarAdminNamespaceToolBuilder) handleNamespaceCreate(_ context.Context, client cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - namespace, err := request.RequireString("namespace") +func (b *PulsarAdminNamespaceToolBuilder) handleNamespaceCreate(_ context.Context, client cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + namespace, err := adapter.RequireString(request, "namespace") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get namespace name: %v", err)), nil + return adapter.NewErrorResult("Failed to get namespace name: %v", err), nil } // Get optional parameters - bundlesStr := request.GetString("bundles", "") + bundlesStr := adapter.GetString(request, "bundles", "") bundles := 0 if bundlesStr != "" { bundlesInt, err := strconv.Atoi(bundlesStr) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid bundles value, must be an integer: %v", err)), nil + return adapter.NewErrorResult("Invalid bundles value, must be an integer: %v", err), nil } bundles = bundlesInt } - clusters := request.GetStringSlice("clusters", []string{}) + clusters := adapter.GetStringSlice(request, "clusters", []string{}) // Prepare policies policies := utils.NewDefaultPolicies() @@ -271,7 +271,7 @@ func (b *PulsarAdminNamespaceToolBuilder) handleNamespaceCreate(_ context.Contex // Set bundles if provided if bundles > 0 { if bundles < 0 || bundles > int(^uint32(0)) { // MaxInt32 - return mcp.NewToolResultError( + return adapter.NewErrorResult( fmt.Sprintf("Invalid number of bundles. Number of bundles has to be in the range of (0, %d].", int(^uint32(0))), ), nil } @@ -286,7 +286,7 @@ func (b *PulsarAdminNamespaceToolBuilder) handleNamespaceCreate(_ context.Contex // Create namespace ns, err := utils.GetNamespaceName(namespace) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid namespace name: %v", err)), nil + return adapter.NewErrorResult("Invalid namespace name: %v", err), nil } err = client.Namespaces().CreateNsWithPolices(ns.String(), *policies) @@ -294,14 +294,14 @@ func (b *PulsarAdminNamespaceToolBuilder) handleNamespaceCreate(_ context.Contex return b.handleError("create namespace", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Created %s successfully", namespace)), nil + return adapter.NewTextResult(fmt.Sprintf("Created %s successfully", namespace)), nil } // handleNamespaceDelete handles deleting a namespace -func (b *PulsarAdminNamespaceToolBuilder) handleNamespaceDelete(_ context.Context, client cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - namespace, err := request.RequireString("namespace") +func (b *PulsarAdminNamespaceToolBuilder) handleNamespaceDelete(_ context.Context, client cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + namespace, err := adapter.RequireString(request, "namespace") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get namespace name: %v", err)), nil + return adapter.NewErrorResult("Failed to get namespace name: %v", err), nil } // Delete namespace @@ -310,25 +310,25 @@ func (b *PulsarAdminNamespaceToolBuilder) handleNamespaceDelete(_ context.Contex return b.handleError("delete namespace", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Deleted %s successfully", namespace)), nil + return adapter.NewTextResult(fmt.Sprintf("Deleted %s successfully", namespace)), nil } // handleClearBacklog handles clearing the backlog for all topics in a namespace -func (b *PulsarAdminNamespaceToolBuilder) handleClearBacklog(_ context.Context, client cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - namespace, err := request.RequireString("namespace") +func (b *PulsarAdminNamespaceToolBuilder) handleClearBacklog(_ context.Context, client cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + namespace, err := adapter.RequireString(request, "namespace") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get namespace name: %v", err)), nil + return adapter.NewErrorResult("Failed to get namespace name: %v", err), nil } // Get optional parameters - subscription := request.GetString("subscription", "") - bundle := request.GetString("bundle", "") - force := request.GetString("force", "") + subscription := adapter.GetString(request, "subscription", "") + bundle := adapter.GetString(request, "bundle", "") + force := adapter.GetString(request, "force", "") forceFlag := force == "true" // If not forced, return an error requiring explicit force flag if !forceFlag { - return mcp.NewToolResultError( + return adapter.NewErrorResult( "Clear backlog operation requires explicit confirmation. Please set force=true to proceed.", ), nil } @@ -336,7 +336,7 @@ func (b *PulsarAdminNamespaceToolBuilder) handleClearBacklog(_ context.Context, // Get namespace name ns, err := utils.GetNamespaceName(namespace) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid namespace name: %v", err)), nil + return adapter.NewErrorResult("Invalid namespace name: %v", err), nil } // Handle different backlog clearing scenarios @@ -358,30 +358,30 @@ func (b *PulsarAdminNamespaceToolBuilder) handleClearBacklog(_ context.Context, return b.handleError("clear backlog", clearErr), nil } - return mcp.NewToolResultText( + return adapter.NewTextResult( fmt.Sprintf("Successfully cleared backlog for all topics in namespace %s", namespace), ), nil } // handleUnsubscribe handles unsubscribing the specified subscription for all topics of a namespace -func (b *PulsarAdminNamespaceToolBuilder) handleUnsubscribe(_ context.Context, client cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - namespace, err := request.RequireString("namespace") +func (b *PulsarAdminNamespaceToolBuilder) handleUnsubscribe(_ context.Context, client cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + namespace, err := adapter.RequireString(request, "namespace") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get namespace name: %v", err)), nil + return adapter.NewErrorResult("Failed to get namespace name: %v", err), nil } - subscription, err := request.RequireString("subscription") + subscription, err := adapter.RequireString(request, "subscription") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get subscription name: %v", err)), nil + return adapter.NewErrorResult("Failed to get subscription name: %v", err), nil } // Get optional bundle - bundle := request.GetString("bundle", "") + bundle := adapter.GetString(request, "bundle", "") // Get namespace name ns, err := utils.GetNamespaceName(namespace) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid namespace name: %v", err)), nil + return adapter.NewErrorResult("Invalid namespace name: %v", err), nil } // Unsubscribe namespace @@ -397,27 +397,27 @@ func (b *PulsarAdminNamespaceToolBuilder) handleUnsubscribe(_ context.Context, c } if bundle == "" { - return mcp.NewToolResultText( + return adapter.NewTextResult( fmt.Sprintf("Successfully unsubscribed the subscription %s for all topics of the namespace %s", subscription, namespace), ), nil } - return mcp.NewToolResultText( + return adapter.NewTextResult( fmt.Sprintf("Successfully unsubscribed the subscription %s for all topics of the namespace %s with bundle range %s", subscription, namespace, bundle), ), nil } // handleUnload handles unloading a namespace from the current serving broker -func (b *PulsarAdminNamespaceToolBuilder) handleUnload(_ context.Context, client cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - namespace, err := request.RequireString("namespace") +func (b *PulsarAdminNamespaceToolBuilder) handleUnload(_ context.Context, client cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + namespace, err := adapter.RequireString(request, "namespace") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get namespace name: %v", err)), nil + return adapter.NewErrorResult("Failed to get namespace name: %v", err), nil } // Get optional bundle - bundle := request.GetString("bundle", "") + bundle := adapter.GetString(request, "bundle", "") // Unload namespace var unloadErr error @@ -432,30 +432,30 @@ func (b *PulsarAdminNamespaceToolBuilder) handleUnload(_ context.Context, client } if bundle == "" { - return mcp.NewToolResultText( + return adapter.NewTextResult( fmt.Sprintf("Unloaded namespace %s successfully", namespace), ), nil } - return mcp.NewToolResultText( + return adapter.NewTextResult( fmt.Sprintf("Unloaded namespace %s with bundle %s successfully", namespace, bundle), ), nil } // handleSplitBundle handles splitting a namespace bundle -func (b *PulsarAdminNamespaceToolBuilder) handleSplitBundle(_ context.Context, client cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - namespace, err := request.RequireString("namespace") +func (b *PulsarAdminNamespaceToolBuilder) handleSplitBundle(_ context.Context, client cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + namespace, err := adapter.RequireString(request, "namespace") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get namespace name: %v", err)), nil + return adapter.NewErrorResult("Failed to get namespace name: %v", err), nil } - bundle, err := request.RequireString("bundle") + bundle, err := adapter.RequireString(request, "bundle") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get bundle: %v", err)), nil + return adapter.NewErrorResult("Failed to get bundle: %v", err), nil } // Get optional unload flag - unload := request.GetString("unload", "") == "true" + unload := adapter.GetString(request, "unload", "") == "true" // Split namespace bundle err = client.Namespaces().SplitNamespaceBundle(namespace, bundle, unload) @@ -463,7 +463,7 @@ func (b *PulsarAdminNamespaceToolBuilder) handleSplitBundle(_ context.Context, c return b.handleError("split namespace bundle", err), nil } - return mcp.NewToolResultText( + return adapter.NewTextResult( fmt.Sprintf("Split namespace bundle %s successfully", bundle), ), nil } diff --git a/pkg/mcp/builders/pulsar/namespace_policy.go b/pkg/mcp/builders/pulsar/namespace_policy.go index 3410dff..98f6c09 100644 --- a/pkg/mcp/builders/pulsar/namespace_policy.go +++ b/pkg/mcp/builders/pulsar/namespace_policy.go @@ -22,11 +22,11 @@ import ( "strings" "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/pulsarctl/pkg/cmdutils" pulsarctlutils "github.com/streamnative/pulsarctl/pkg/ctl/utils" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" mcpCtx "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" ) @@ -59,7 +59,7 @@ func NewPulsarAdminNamespacePolicyToolBuilder() *PulsarAdminNamespacePolicyToolB } // BuildTools builds the Pulsar admin namespace policy tool list -func (b *PulsarAdminNamespacePolicyToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]server.ServerTool, error) { +func (b *PulsarAdminNamespacePolicyToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]builders.ServerTool, error) { // Check features - return empty list if no required features are present if !b.HasAnyRequiredFeature(config.Features) { return nil, nil @@ -71,12 +71,12 @@ func (b *PulsarAdminNamespacePolicyToolBuilder) BuildTools(_ context.Context, co } // Build tools - tools := []server.ServerTool{} + tools := []builders.ServerTool{} // Always add get policies tool getTool := b.buildNamespaceGetPoliciesTool() getHandler := b.buildNamespaceGetPoliciesHandler() - tools = append(tools, server.ServerTool{ + tools = append(tools, builders.ServerTool{ Tool: getTool, Handler: getHandler, }) @@ -86,7 +86,7 @@ func (b *PulsarAdminNamespacePolicyToolBuilder) BuildTools(_ context.Context, co // Add set policy tool setTool := b.buildNamespaceSetPolicyTool() setHandler := b.buildNamespaceSetPolicyHandler() - tools = append(tools, server.ServerTool{ + tools = append(tools, builders.ServerTool{ Tool: setTool, Handler: setHandler, }) @@ -94,7 +94,7 @@ func (b *PulsarAdminNamespacePolicyToolBuilder) BuildTools(_ context.Context, co // Add remove policy tool removeTool := b.buildNamespaceRemovePolicyTool() removeHandler := b.buildNamespaceRemovePolicyHandler() - tools = append(tools, server.ServerTool{ + tools = append(tools, builders.ServerTool{ Tool: removeTool, Handler: removeHandler, }) @@ -104,7 +104,7 @@ func (b *PulsarAdminNamespacePolicyToolBuilder) BuildTools(_ context.Context, co } // buildNamespaceGetPoliciesTool builds the get policies tool -func (b *PulsarAdminNamespacePolicyToolBuilder) buildNamespaceGetPoliciesTool() mcp.Tool { +func (b *PulsarAdminNamespacePolicyToolBuilder) buildNamespaceGetPoliciesTool() *mcpsdk.Tool { toolDesc := "Get the configuration policies of a namespace. " + "Returns a comprehensive view of all policies applied to the namespace. " + "The response includes the following fields:" + @@ -138,16 +138,16 @@ func (b *PulsarAdminNamespacePolicyToolBuilder) buildNamespaceGetPoliciesTool() "\n* is_allow_auto_update_schema: Whether automatic schema updates are allowed" + "\nRequires tenant admin permissions." - return mcp.NewTool("pulsar_admin_namespace_policy_get", - mcp.WithDescription(toolDesc), - mcp.WithString("namespace", mcp.Required(), - mcp.Description("The namespace name (tenant/namespace) to get policies for"), + return builders.NewTool("pulsar_admin_namespace_policy_get", + builders.WithDescription(toolDesc), + builders.WithString("namespace", builders.Required(), + builders.Description("The namespace name (tenant/namespace) to get policies for"), ), ) } // buildNamespaceSetPolicyTool builds the set policy tool -func (b *PulsarAdminNamespacePolicyToolBuilder) buildNamespaceSetPolicyTool() mcp.Tool { +func (b *PulsarAdminNamespacePolicyToolBuilder) buildNamespaceSetPolicyTool() *mcpsdk.Tool { toolDesc := "Set a policy for a namespace. " + "This is a unified tool for setting different types of policies on a namespace. " + "The policy type determines which specific policy will be set, and the required parameters " + @@ -173,13 +173,13 @@ func (b *PulsarAdminNamespacePolicyToolBuilder) buildNamespaceSetPolicyTool() mc "subscription-auth-mode, subscription-permission, dispatch-rate, replicator-dispatch-rate, subscribe-rate, " + "subscription-dispatch-rate, publish-rate" - return mcp.NewTool("pulsar_admin_namespace_policy_set", - mcp.WithDescription(toolDesc), - mcp.WithString("namespace", mcp.Required(), - mcp.Description("The namespace name (tenant/namespace) to set the policy for"), + return builders.NewTool("pulsar_admin_namespace_policy_set", + builders.WithDescription(toolDesc), + builders.WithString("namespace", builders.Required(), + builders.Description("The namespace name (tenant/namespace) to set the policy for"), ), - mcp.WithString("policy", mcp.Required(), - mcp.Description("Type of policy to set. Available options: "+ + builders.WithString("policy", builders.Required(), + builders.Description("Type of policy to set. Available options: "+ "message-ttl, retention, permission, replication-clusters, backlog-quota, "+ "topic-auto-creation, schema-validation, schema-auto-update, auto-update-schema, "+ "offload-threshold, offload-deletion-lag, compaction-threshold, "+ @@ -189,60 +189,60 @@ func (b *PulsarAdminNamespacePolicyToolBuilder) buildNamespaceSetPolicyTool() mc "replicator-dispatch-rate, subscribe-rate, subscription-dispatch-rate, publish-rate"), ), // Generic policy parameters - specific ones will be used based on the policy type - mcp.WithString("role", - mcp.Description("Role name for permission policies"), + builders.WithString("role", + builders.Description("Role name for permission policies"), ), - mcp.WithArray("actions", - mcp.Description("Actions to grant for permission policies (e.g., produce, consume)"), - mcp.Items( + builders.WithArray("actions", + builders.Description("Actions to grant for permission policies (e.g., produce, consume)"), + builders.Items( map[string]interface{}{ "type": "string", "description": "action", }, ), ), - mcp.WithArray("clusters", - mcp.Description("List of clusters for replication policies"), - mcp.Items( + builders.WithArray("clusters", + builders.Description("List of clusters for replication policies"), + builders.Items( map[string]interface{}{ "type": "string", "description": "cluster", }, ), ), - mcp.WithArray("roles", - mcp.Description("List of roles for subscription permission policies"), - mcp.Items( + builders.WithArray("roles", + builders.Description("List of roles for subscription permission policies"), + builders.Items( map[string]interface{}{ "type": "string", "description": "role", }, ), ), - mcp.WithString("ttl", - mcp.Description("Message TTL in seconds (or 0 to disable TTL)"), + builders.WithString("ttl", + builders.Description("Message TTL in seconds (or 0 to disable TTL)"), ), - mcp.WithString("time", - mcp.Description("Retention time in minutes, or special values: 0 (no retention) or -1 (infinite retention)"), + builders.WithString("time", + builders.Description("Retention time in minutes, or special values: 0 (no retention) or -1 (infinite retention)"), ), - mcp.WithString("size", - mcp.Description("Retention size limit (e.g., 10M, 16G, 3T), or special values: 0 (no retention) or -1 (infinite size retention)"), + builders.WithString("size", + builders.Description("Retention size limit (e.g., 10M, 16G, 3T), or special values: 0 (no retention) or -1 (infinite size retention)"), ), - mcp.WithString("limit-size", - mcp.Description("Size limit for backlog quota (e.g., 10M, 16G)"), + builders.WithString("limit-size", + builders.Description("Size limit for backlog quota (e.g., 10M, 16G)"), ), - mcp.WithString("limit-time", - mcp.Description("Time limit in seconds for backlog quota. Default is -1 (infinite)"), + builders.WithString("limit-time", + builders.Description("Time limit in seconds for backlog quota. Default is -1 (infinite)"), ), - mcp.WithString("policy", - mcp.Description("Retention policy for backlog quota (valid options: producer_request_hold, producer_exception, consumer_backlog_eviction)"), + builders.WithString("policy", + builders.Description("Retention policy for backlog quota (valid options: producer_request_hold, producer_exception, consumer_backlog_eviction)"), ), // Add more parameters as needed ) } // buildNamespaceRemovePolicyTool builds the remove policy tool -func (b *PulsarAdminNamespacePolicyToolBuilder) buildNamespaceRemovePolicyTool() mcp.Tool { +func (b *PulsarAdminNamespacePolicyToolBuilder) buildNamespaceRemovePolicyTool() *mcpsdk.Tool { toolDesc := "Remove a policy from a namespace. " + "This is a unified tool for removing different types of policies from a namespace. " + "The policy type determines which specific policy will be removed. " + @@ -262,35 +262,35 @@ func (b *PulsarAdminNamespacePolicyToolBuilder) buildNamespaceRemovePolicyTool() "6. subscription-permission: Revokes permission from a role to access a subscription\n" + " - Required: namespace, subscription, role" - return mcp.NewTool("pulsar_admin_namespace_policy_remove", - mcp.WithDescription(toolDesc), - mcp.WithString("namespace", mcp.Required(), - mcp.Description("The namespace name (tenant/namespace) to remove the policy from"), + return builders.NewTool("pulsar_admin_namespace_policy_remove", + builders.WithDescription(toolDesc), + builders.WithString("namespace", builders.Required(), + builders.Description("The namespace name (tenant/namespace) to remove the policy from"), ), - mcp.WithString("policy", mcp.Required(), - mcp.Description("Type of policy to remove. Available options: "+ + builders.WithString("policy", builders.Required(), + builders.Description("Type of policy to remove. Available options: "+ "backlog-quota, topic-auto-creation, offload-deletion-lag, anti-affinity-group, "+ "permission, subscription-permission"), ), - mcp.WithString("role", - mcp.Description("Role name for permission policies"), + builders.WithString("role", + builders.Description("Role name for permission policies"), ), - mcp.WithString("subscription", - mcp.Description("Subscription name for subscription permission policies"), + builders.WithString("subscription", + builders.Description("Subscription name for subscription permission policies"), ), - mcp.WithString("type", - mcp.Description("Type of backlog quota to remove"), + builders.WithString("type", + builders.Description("Type of backlog quota to remove"), ), ) } // buildNamespaceGetPoliciesHandler builds the get policies handler -func (b *PulsarAdminNamespacePolicyToolBuilder) buildNamespaceGetPoliciesHandler() func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminNamespacePolicyToolBuilder) buildNamespaceGetPoliciesHandler() func(context.Context, *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get Pulsar session from context session := mcpCtx.GetPulsarSession(ctx) if session == nil { - return mcp.NewToolResultError("Pulsar session not found in context"), nil + return adapter.NewErrorResult("Pulsar session not found in context"), nil } client, err := session.GetAdminClient() @@ -298,15 +298,15 @@ func (b *PulsarAdminNamespacePolicyToolBuilder) buildNamespaceGetPoliciesHandler return b.handleError("get admin client", err), nil } - namespace, err := request.RequireString("namespace") + namespace, err := adapter.RequireString(request, "namespace") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get namespace name: %v", err)), nil + return adapter.NewErrorResult("Failed to get namespace name: %v", err), nil } // Get policies policies, err := client.Namespaces().GetPolicies(namespace) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get policies: %v", err)), nil + return adapter.NewErrorResult("Failed to get policies: %v", err), nil } return b.marshalResponse(policies) @@ -314,12 +314,12 @@ func (b *PulsarAdminNamespacePolicyToolBuilder) buildNamespaceGetPoliciesHandler } // buildNamespaceSetPolicyHandler builds the set policy handler -func (b *PulsarAdminNamespacePolicyToolBuilder) buildNamespaceSetPolicyHandler() func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminNamespacePolicyToolBuilder) buildNamespaceSetPolicyHandler() func(context.Context, *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get Pulsar session from context session := mcpCtx.GetPulsarSession(ctx) if session == nil { - return mcp.NewToolResultError("Pulsar session not found in context"), nil + return adapter.NewErrorResult("Pulsar session not found in context"), nil } client, err := session.GetAdminClient() @@ -327,14 +327,14 @@ func (b *PulsarAdminNamespacePolicyToolBuilder) buildNamespaceSetPolicyHandler() return b.handleError("get admin client", err), nil } - namespace, err := request.RequireString("namespace") + namespace, err := adapter.RequireString(request, "namespace") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get namespace name: %v", err)), nil + return adapter.NewErrorResult("Failed to get namespace name: %v", err), nil } - policy, err := request.RequireString("policy") + policy, err := adapter.RequireString(request, "policy") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get policy type: %v", err)), nil + return adapter.NewErrorResult("Failed to get policy type: %v", err), nil } // Handle different policy types @@ -351,18 +351,18 @@ func (b *PulsarAdminNamespacePolicyToolBuilder) buildNamespaceSetPolicyHandler() return b.handleSetBacklogQuota(ctx, client, namespace, request) // Add more policy types as needed default: - return mcp.NewToolResultError(fmt.Sprintf("Unsupported policy type: %s", policy)), nil + return adapter.NewErrorResult("Unsupported policy type: %s", policy), nil } } } // buildNamespaceRemovePolicyHandler builds the remove policy handler -func (b *PulsarAdminNamespacePolicyToolBuilder) buildNamespaceRemovePolicyHandler() func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminNamespacePolicyToolBuilder) buildNamespaceRemovePolicyHandler() func(context.Context, *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get Pulsar session from context session := mcpCtx.GetPulsarSession(ctx) if session == nil { - return mcp.NewToolResultError("Pulsar session not found in context"), nil + return adapter.NewErrorResult("Pulsar session not found in context"), nil } client, err := session.GetAdminClient() @@ -370,14 +370,14 @@ func (b *PulsarAdminNamespacePolicyToolBuilder) buildNamespaceRemovePolicyHandle return b.handleError("get admin client", err), nil } - namespace, err := request.RequireString("namespace") + namespace, err := adapter.RequireString(request, "namespace") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get namespace name: %v", err)), nil + return adapter.NewErrorResult("Failed to get namespace name: %v", err), nil } - policy, err := request.RequireString("policy") + policy, err := adapter.RequireString(request, "policy") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get policy type: %v", err)), nil + return adapter.NewErrorResult("Failed to get policy type: %v", err), nil } // Handle different policy types @@ -388,54 +388,54 @@ func (b *PulsarAdminNamespacePolicyToolBuilder) buildNamespaceRemovePolicyHandle return b.handleRemoveBacklogQuota(ctx, client, namespace, request) // Add more policy types as needed default: - return mcp.NewToolResultError(fmt.Sprintf("Unsupported policy type for removal: %s", policy)), nil + return adapter.NewErrorResult("Unsupported policy type for removal: %s", policy), nil } } } // Utility functions -func (b *PulsarAdminNamespacePolicyToolBuilder) handleError(operation string, err error) *mcp.CallToolResult { - return mcp.NewToolResultError(fmt.Sprintf("Failed to %s: %v", operation, err)) +func (b *PulsarAdminNamespacePolicyToolBuilder) handleError(operation string, err error) *mcpsdk.CallToolResult { + return adapter.NewErrorResult("Failed to %s: %v", operation, err) } -func (b *PulsarAdminNamespacePolicyToolBuilder) marshalResponse(data interface{}) (*mcp.CallToolResult, error) { +func (b *PulsarAdminNamespacePolicyToolBuilder) marshalResponse(data interface{}) (*mcpsdk.CallToolResult, error) { jsonBytes, err := json.Marshal(data) if err != nil { return b.handleError("marshal response", err), nil } - return mcp.NewToolResultText(string(jsonBytes)), nil + return adapter.NewTextResult(string(jsonBytes)), nil } // Policy-specific handler functions // handleSetMessageTTL handles setting message TTL for a namespace -func (b *PulsarAdminNamespacePolicyToolBuilder) handleSetMessageTTL(_ context.Context, client cmdutils.Client, namespace string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - ttlStr, err := request.RequireString("ttl") +func (b *PulsarAdminNamespacePolicyToolBuilder) handleSetMessageTTL(_ context.Context, client cmdutils.Client, namespace string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + ttlStr, err := adapter.RequireString(request, "ttl") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get TTL: %v", err)), nil + return adapter.NewErrorResult("Failed to get TTL: %v", err), nil } ttl, err := strconv.ParseInt(ttlStr, 10, 64) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid TTL value, must be an integer: %v", err)), nil + return adapter.NewErrorResult("Invalid TTL value, must be an integer: %v", err), nil } // Set message TTL err = client.Namespaces().SetNamespaceMessageTTL(namespace, int(ttl)) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to set message TTL: %v", err)), nil + return adapter.NewErrorResult("Failed to set message TTL: %v", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Set message TTL for %s to %d seconds", namespace, ttl)), nil + return adapter.NewTextResult(fmt.Sprintf("Set message TTL for %s to %d seconds", namespace, ttl)), nil } // handleSetRetention handles setting retention for a namespace -func (b *PulsarAdminNamespacePolicyToolBuilder) handleSetRetention(_ context.Context, client cmdutils.Client, namespace string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - timeStr := request.GetString("time", "") - sizeStr := request.GetString("size", "") +func (b *PulsarAdminNamespacePolicyToolBuilder) handleSetRetention(_ context.Context, client cmdutils.Client, namespace string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + timeStr := adapter.GetString(request, "time", "") + sizeStr := adapter.GetString(request, "size", "") if timeStr == "" && sizeStr == "" { - return mcp.NewToolResultError("At least one of 'time' or 'size' must be specified"), nil + return adapter.NewErrorResult("At least one of 'time' or 'size' must be specified"), nil } // Parse retention time @@ -444,7 +444,7 @@ func (b *PulsarAdminNamespacePolicyToolBuilder) handleSetRetention(_ context.Con // Parse relative time in seconds from the input string retentionTime, err := pulsarctlutils.ParseRelativeTimeInSeconds(timeStr) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid retention time format: %v", err)), nil + return adapter.NewErrorResult("Invalid retention time format: %v", err), nil } if retentionTime != -1 { @@ -466,14 +466,14 @@ func (b *PulsarAdminNamespacePolicyToolBuilder) handleSetRetention(_ context.Con // Parse size string (e.g., "10M", "16G", "3T") sizeInBytes, err := pulsarctlutils.ValidateSizeString(sizeStr) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid retention size format: %v", err)), nil + return adapter.NewErrorResult("Invalid retention size format: %v", err), nil } if sizeInBytes != -1 { // Convert bytes to MB retentionSizeInMB = int(sizeInBytes / (1024 * 1024)) if retentionSizeInMB < 1 { - return mcp.NewToolResultError("Retention size must be at least 1MB"), nil + return adapter.NewErrorResult("Retention size must be at least 1MB"), nil } } else { retentionSizeInMB = -1 // Infinite size retention @@ -489,122 +489,122 @@ func (b *PulsarAdminNamespacePolicyToolBuilder) handleSetRetention(_ context.Con // Set retention err := client.Namespaces().SetRetention(namespace, retention) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to set retention: %v", err)), nil + return adapter.NewErrorResult("Failed to set retention: %v", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Set retention for %s successfully", namespace)), nil + return adapter.NewTextResult(fmt.Sprintf("Set retention for %s successfully", namespace)), nil } // handleGrantPermission handles granting permissions on a namespace -func (b *PulsarAdminNamespacePolicyToolBuilder) handleGrantPermission(_ context.Context, client cmdutils.Client, namespace string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - role, err := request.RequireString("role") +func (b *PulsarAdminNamespacePolicyToolBuilder) handleGrantPermission(_ context.Context, client cmdutils.Client, namespace string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + role, err := adapter.RequireString(request, "role") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get role: %v", err)), nil + return adapter.NewErrorResult("Failed to get role: %v", err), nil } - actions, err := request.RequireStringSlice("actions") + actions, err := adapter.RequireStringSlice(request, "actions") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get actions: %v", err)), nil + return adapter.NewErrorResult("Failed to get actions: %v", err), nil } ns, err := utils.GetNamespaceName(namespace) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid namespace name: %v", err)), nil + return adapter.NewErrorResult("Invalid namespace name: %v", err), nil } a, err := b.parseActions(actions) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to parse actions: %v", err)), nil + return adapter.NewErrorResult("Failed to parse actions: %v", err), nil } // Grant permissions err = client.Namespaces().GrantNamespacePermission(*ns, role, a) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to grant permission: %v", err)), nil + return adapter.NewErrorResult("Failed to grant permission: %v", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Granted %v permission(s) to role %s on %s", actions, role, namespace)), nil + return adapter.NewTextResult(fmt.Sprintf("Granted %v permission(s) to role %s on %s", actions, role, namespace)), nil } // handleRevokePermission handles revoking permissions from a namespace -func (b *PulsarAdminNamespacePolicyToolBuilder) handleRevokePermission(_ context.Context, client cmdutils.Client, namespace string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - role, err := request.RequireString("role") +func (b *PulsarAdminNamespacePolicyToolBuilder) handleRevokePermission(_ context.Context, client cmdutils.Client, namespace string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + role, err := adapter.RequireString(request, "role") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get role: %v", err)), nil + return adapter.NewErrorResult("Failed to get role: %v", err), nil } ns, err := utils.GetNamespaceName(namespace) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid namespace name: %v", err)), nil + return adapter.NewErrorResult("Invalid namespace name: %v", err), nil } // Revoke permissions err = client.Namespaces().RevokeNamespacePermission(*ns, role) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to revoke permission: %v", err)), nil + return adapter.NewErrorResult("Failed to revoke permission: %v", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Revoked all permissions from role %s on %s", role, namespace)), nil + return adapter.NewTextResult(fmt.Sprintf("Revoked all permissions from role %s on %s", role, namespace)), nil } // handleSetReplicationClusters handles setting replication clusters for a namespace -func (b *PulsarAdminNamespacePolicyToolBuilder) handleSetReplicationClusters(_ context.Context, client cmdutils.Client, namespace string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - clusters, err := request.RequireStringSlice("clusters") +func (b *PulsarAdminNamespacePolicyToolBuilder) handleSetReplicationClusters(_ context.Context, client cmdutils.Client, namespace string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + clusters, err := adapter.RequireStringSlice(request, "clusters") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get clusters: %v", err)), nil + return adapter.NewErrorResult("Failed to get clusters: %v", err), nil } if len(clusters) == 0 { - return mcp.NewToolResultError("At least one cluster must be specified"), nil + return adapter.NewErrorResult("At least one cluster must be specified"), nil } // Set replication clusters err = client.Namespaces().SetNamespaceReplicationClusters(namespace, clusters) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to set replication clusters: %v", err)), nil + return adapter.NewErrorResult("Failed to set replication clusters: %v", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Set replication clusters for %s to %s", namespace, strings.Join(clusters, ", "))), nil + return adapter.NewTextResult(fmt.Sprintf("Set replication clusters for %s to %s", namespace, strings.Join(clusters, ", "))), nil } // handleSetBacklogQuota handles setting backlog quota for a namespace -func (b *PulsarAdminNamespacePolicyToolBuilder) handleSetBacklogQuota(_ context.Context, client cmdutils.Client, namespace string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - limitSizeStr, err := request.RequireString("limit-size") +func (b *PulsarAdminNamespacePolicyToolBuilder) handleSetBacklogQuota(_ context.Context, client cmdutils.Client, namespace string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + limitSizeStr, err := adapter.RequireString(request, "limit-size") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get limit size: %v", err)), nil + return adapter.NewErrorResult("Failed to get limit size: %v", err), nil } - policyStr, err := request.RequireString("policy") + policyStr, err := adapter.RequireString(request, "policy") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get policy: %v", err)), nil + return adapter.NewErrorResult("Failed to get policy: %v", err), nil } // Parse backlog size limit limitSize, err := pulsarctlutils.ValidateSizeString(limitSizeStr) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid limit size format: %v", err)), nil + return adapter.NewErrorResult("Invalid limit size format: %v", err), nil } // Parse backlog quota policy using the ParseRetentionPolicy function policy, err := utils.ParseRetentionPolicy(policyStr) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid backlog quota policy: %s. Valid options: producer_request_hold, producer_exception, consumer_backlog_eviction", policyStr)), nil + return adapter.NewErrorResult("Invalid backlog quota policy: %s. Valid options: producer_request_hold, producer_exception, consumer_backlog_eviction", policyStr), nil } // Get optional time limit - limitTimeStr := request.GetString("limit-time", "-1") + limitTimeStr := adapter.GetString(request, "limit-time", "-1") limitTime, err := strconv.ParseInt(limitTimeStr, 10, 64) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid limit time: %v", err)), nil + return adapter.NewErrorResult("Invalid limit time: %v", err), nil } // Parse quota type (optional, default to destination_storage) - quotaTypeStr := request.GetString("type", "destination_storage") + quotaTypeStr := adapter.GetString(request, "type", "destination_storage") quotaType := utils.DestinationStorage // Default if quotaTypeStr != "" { parsedType, err := utils.ParseBacklogQuotaType(quotaTypeStr) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid backlog quota type: %v", err)), nil + return adapter.NewErrorResult("Invalid backlog quota type: %v", err), nil } quotaType = parsedType } @@ -613,21 +613,21 @@ func (b *PulsarAdminNamespacePolicyToolBuilder) handleSetBacklogQuota(_ context. backlogQuota := utils.NewBacklogQuota(limitSize, limitTime, policy) err = client.Namespaces().SetBacklogQuota(namespace, backlogQuota, quotaType) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to set backlog quota: %v", err)), nil + return adapter.NewErrorResult("Failed to set backlog quota: %v", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Set backlog quota for %s successfully", namespace)), nil + return adapter.NewTextResult(fmt.Sprintf("Set backlog quota for %s successfully", namespace)), nil } // handleRemoveBacklogQuota handles removing backlog quota for a namespace -func (b *PulsarAdminNamespacePolicyToolBuilder) handleRemoveBacklogQuota(_ context.Context, client cmdutils.Client, namespace string, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminNamespacePolicyToolBuilder) handleRemoveBacklogQuota(_ context.Context, client cmdutils.Client, namespace string, _ *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Remove backlog quota (API doesn't require quota type for removal) err := client.Namespaces().RemoveBacklogQuota(namespace) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to remove backlog quota: %v", err)), nil + return adapter.NewErrorResult("Failed to remove backlog quota: %v", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Removed backlog quota for %s successfully", namespace)), nil + return adapter.NewTextResult(fmt.Sprintf("Removed backlog quota for %s successfully", namespace)), nil } // parseActions parses action strings into AuthAction enums diff --git a/pkg/mcp/builders/pulsar/nsisolationpolicy.go b/pkg/mcp/builders/pulsar/nsisolationpolicy.go index 74dd9aa..579fc63 100644 --- a/pkg/mcp/builders/pulsar/nsisolationpolicy.go +++ b/pkg/mcp/builders/pulsar/nsisolationpolicy.go @@ -21,11 +21,10 @@ import ( "strings" "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/pulsarctl/pkg/cmdutils" - "github.com/streamnative/streamnative-mcp-server/pkg/common" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" mcpCtx "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" ) @@ -58,7 +57,7 @@ func NewPulsarAdminNsIsolationPolicyToolBuilder() *PulsarAdminNsIsolationPolicyT } // BuildTools builds the Pulsar admin namespace isolation policy tool list -func (b *PulsarAdminNsIsolationPolicyToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]server.ServerTool, error) { +func (b *PulsarAdminNsIsolationPolicyToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]builders.ServerTool, error) { // Check features - return empty list if no required features are present if !b.HasAnyRequiredFeature(config.Features) { return nil, nil @@ -73,7 +72,7 @@ func (b *PulsarAdminNsIsolationPolicyToolBuilder) BuildTools(_ context.Context, tool := b.buildNsIsolationPolicyTool() handler := b.buildNsIsolationPolicyHandler(config.ReadOnly) - return []server.ServerTool{ + return []builders.ServerTool{ { Tool: tool, Handler: handler, @@ -82,7 +81,7 @@ func (b *PulsarAdminNsIsolationPolicyToolBuilder) BuildTools(_ context.Context, } // buildNsIsolationPolicyTool builds the Pulsar admin namespace isolation policy MCP tool definition -func (b *PulsarAdminNsIsolationPolicyToolBuilder) buildNsIsolationPolicyTool() mcp.Tool { +func (b *PulsarAdminNsIsolationPolicyToolBuilder) buildNsIsolationPolicyTool() *mcpsdk.Tool { toolDesc := "Manage namespace isolation policies in a Pulsar cluster. " + "Allows viewing, creating, updating, and deleting namespace isolation policies. " + "Some operations require super-user permissions." @@ -98,85 +97,85 @@ func (b *PulsarAdminNsIsolationPolicyToolBuilder) buildNsIsolationPolicyTool() m "- set: Create or update a resource (requires super-user permissions)\n" + "- delete: Delete a resource (requires super-user permissions)" - return mcp.NewTool("pulsar_admin_nsisolationpolicy", - mcp.WithDescription(toolDesc), - mcp.WithString("resource", mcp.Required(), - mcp.Description(resourceDesc), + return builders.NewTool("pulsar_admin_nsisolationpolicy", + builders.WithDescription(toolDesc), + builders.WithString("resource", builders.Required(), + builders.Description(resourceDesc), ), - mcp.WithString("operation", mcp.Required(), - mcp.Description(operationDesc), + builders.WithString("operation", builders.Required(), + builders.Description(operationDesc), ), - mcp.WithString("cluster", mcp.Required(), - mcp.Description("Cluster name"), + builders.WithString("cluster", builders.Required(), + builders.Description("Cluster name"), ), - mcp.WithString("name", - mcp.Description("Name of the policy or broker to operate on, based on resource type.\n"+ + builders.WithString("name", + builders.Description("Name of the policy or broker to operate on, based on resource type.\n"+ "Required for: policy.get, policy.delete, policy.set, broker.get"), ), - mcp.WithArray("namespaces", - mcp.Description("List of namespaces to apply the isolation policy. Required for policy.set"), - mcp.Items( + builders.WithArray("namespaces", + builders.Description("List of namespaces to apply the isolation policy. Required for policy.set"), + builders.Items( map[string]interface{}{ "type": "string", "description": "namespace", }, ), ), - mcp.WithArray("primary", - mcp.Description("List of primary brokers for the namespaces. Required for policy.set"), - mcp.Items( + builders.WithArray("primary", + builders.Description("List of primary brokers for the namespaces. Required for policy.set"), + builders.Items( map[string]interface{}{ "type": "string", "description": "primary broker", }, ), ), - mcp.WithArray("secondary", - mcp.Description("List of secondary brokers for the namespaces. Optional for policy.set"), - mcp.Items( + builders.WithArray("secondary", + builders.Description("List of secondary brokers for the namespaces. Optional for policy.set"), + builders.Items( map[string]interface{}{ "type": "string", "description": "secondary broker", }, ), ), - mcp.WithString("autoFailoverPolicyType", - mcp.Description("Auto failover policy type (e.g., min_available). Optional for policy.set"), + builders.WithString("autoFailoverPolicyType", + builders.Description("Auto failover policy type (e.g., min_available). Optional for policy.set"), ), - mcp.WithObject("autoFailoverPolicyParams", - mcp.Description("Auto failover policy parameters as an object (e.g., {'min_limit': '1', 'usage_threshold': '100'}). Optional for policy.set"), + builders.WithObject("autoFailoverPolicyParams", + builders.Description("Auto failover policy parameters as an object (e.g., {'min_limit': '1', 'usage_threshold': '100'}). Optional for policy.set"), ), ) } // buildNsIsolationPolicyHandler builds the Pulsar admin namespace isolation policy handler function -func (b *PulsarAdminNsIsolationPolicyToolBuilder) buildNsIsolationPolicyHandler(readOnly bool) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminNsIsolationPolicyToolBuilder) buildNsIsolationPolicyHandler(readOnly bool) func(context.Context, *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get Pulsar session from context session := mcpCtx.GetPulsarSession(ctx) if session == nil { - return mcp.NewToolResultError("Pulsar session not found in context"), nil + return adapter.NewErrorResult("Pulsar session not found in context"), nil } client, err := session.GetAdminClient() if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get admin client: %v", err)), nil + return adapter.NewErrorResult("Failed to get admin client: %v", err), nil } // Get required parameters - resource, err := request.RequireString("resource") + resource, err := adapter.RequireString(request, "resource") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get resource: %v", err)), nil + return adapter.NewErrorResult("Failed to get resource: %v", err), nil } - operation, err := request.RequireString("operation") + operation, err := adapter.RequireString(request, "operation") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get operation: %v", err)), nil + return adapter.NewErrorResult("Failed to get operation: %v", err), nil } - cluster, err := request.RequireString("cluster") + cluster, err := adapter.RequireString(request, "cluster") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get cluster name: %v", err)), nil + return adapter.NewErrorResult("Failed to get cluster name: %v", err), nil } // Normalize parameters @@ -185,7 +184,7 @@ func (b *PulsarAdminNsIsolationPolicyToolBuilder) buildNsIsolationPolicyHandler( // Validate write operations in read-only mode if readOnly && (operation == "set" || operation == "delete") { - return mcp.NewToolResultError("Write operations are not allowed in read-only mode"), nil + return adapter.NewErrorResult("Write operations are not allowed in read-only mode"), nil } // Dispatch based on resource type @@ -197,7 +196,7 @@ func (b *PulsarAdminNsIsolationPolicyToolBuilder) buildNsIsolationPolicyHandler( case "brokers": return b.handleNsIsolationBrokersResource(client, operation, cluster, request) default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid resource: %s. Available resources: policy, broker, brokers", resource)), nil + return adapter.NewErrorResult("Invalid resource: %s. Available resources: policy, broker, brokers", resource), nil } } } @@ -205,80 +204,80 @@ func (b *PulsarAdminNsIsolationPolicyToolBuilder) buildNsIsolationPolicyHandler( // Helper functions // handlePolicyResource handles operations on the "policy" resource -func (b *PulsarAdminNsIsolationPolicyToolBuilder) handlePolicyResource(client cmdutils.Client, operation, cluster string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminNsIsolationPolicyToolBuilder) handlePolicyResource(client cmdutils.Client, operation, cluster string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { switch operation { case "get": - name, err := request.RequireString("name") + name, err := adapter.RequireString(request, "name") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'name' for policy.get: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'name' for policy.get: %v", err), nil } // Get namespace isolation policy policyInfo, err := client.NsIsolationPolicy().GetNamespaceIsolationPolicy(cluster, name) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get namespace isolation policy: %v", err)), nil + return adapter.NewErrorResult("Failed to get namespace isolation policy: %v", err), nil } // Convert result to JSON string policyInfoJSON, err := json.Marshal(policyInfo) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to serialize namespace isolation policy: %v", err)), nil + return adapter.NewErrorResult("Failed to serialize namespace isolation policy: %v", err), nil } - return mcp.NewToolResultText(string(policyInfoJSON)), nil + return adapter.NewTextResult(string(policyInfoJSON)), nil case "list": // Get namespace isolation policies policies, err := client.NsIsolationPolicy().GetNamespaceIsolationPolicies(cluster) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list namespace isolation policies: %v", err)), nil + return adapter.NewErrorResult("Failed to list namespace isolation policies: %v", err), nil } // Convert result to JSON string policiesJSON, err := json.Marshal(policies) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to serialize namespace isolation policies: %v", err)), nil + return adapter.NewErrorResult("Failed to serialize namespace isolation policies: %v", err), nil } - return mcp.NewToolResultText(string(policiesJSON)), nil + return adapter.NewTextResult(string(policiesJSON)), nil case "delete": - name, err := request.RequireString("name") + name, err := adapter.RequireString(request, "name") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'name' for policy.delete: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'name' for policy.delete: %v", err), nil } // Delete namespace isolation policy err = client.NsIsolationPolicy().DeleteNamespaceIsolationPolicy(cluster, name) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to delete namespace isolation policy: %v", err)), nil + return adapter.NewErrorResult("Failed to delete namespace isolation policy: %v", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Delete namespace isolation policy %s successfully", name)), nil + return adapter.NewTextResult(fmt.Sprintf("Delete namespace isolation policy %s successfully", name)), nil case "set": - name, err := request.RequireString("name") + name, err := adapter.RequireString(request, "name") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'name' for policy.set: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'name' for policy.set: %v", err), nil } - namespaces, err := request.RequireStringSlice("namespaces") + namespaces, err := adapter.RequireStringSlice(request, "namespaces") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'namespaces' for policy.set: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'namespaces' for policy.set: %v", err), nil } - primary, err := request.RequireStringSlice("primary") + primary, err := adapter.RequireStringSlice(request, "primary") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'primary' for policy.set: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'primary' for policy.set: %v", err), nil } - secondary := request.GetStringSlice("secondary", []string{}) - autoFailoverPolicyType := request.GetString("autoFailoverPolicyType", "") + secondary := adapter.GetStringSlice(request, "secondary", []string{}) + autoFailoverPolicyType := adapter.GetString(request, "autoFailoverPolicyType", "") // Parse autoFailoverPolicyParams as a map - autoFailoverPolicyParamsRaw, ok := common.OptionalParamObject(request.GetArguments(), "autoFailoverPolicyParams") - if !ok { - return mcp.NewToolResultError("Failed to extract autoFailoverPolicyParams"), nil + autoFailoverPolicyParamsRaw := adapter.GetObject(request, "autoFailoverPolicyParams") + if autoFailoverPolicyParamsRaw == nil { + return adapter.NewErrorResult("Failed to extract autoFailoverPolicyParams"), nil } autoFailoverPolicyParams := make(map[string]string) @@ -292,69 +291,69 @@ func (b *PulsarAdminNsIsolationPolicyToolBuilder) handlePolicyResource(client cm nsIsolationData, err := utils.CreateNamespaceIsolationData(namespaces, primary, secondary, autoFailoverPolicyType, autoFailoverPolicyParams) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to create namespace isolation data: %v", err)), nil + return adapter.NewErrorResult("Failed to create namespace isolation data: %v", err), nil } // Create/update namespace isolation policy err = client.NsIsolationPolicy().CreateNamespaceIsolationPolicy(cluster, name, *nsIsolationData) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to create/update namespace isolation policy: %v", err)), nil + return adapter.NewErrorResult("Failed to create/update namespace isolation policy: %v", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Create/Update namespace isolation policy %s successfully", name)), nil + return adapter.NewTextResult(fmt.Sprintf("Create/Update namespace isolation policy %s successfully", name)), nil default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid operation for resource 'policy': %s. Available operations: get, list, delete, set", operation)), nil + return adapter.NewErrorResult("Invalid operation for resource 'policy': %s. Available operations: get, list, delete, set", operation), nil } } // handleBrokerResource handles operations on the "broker" resource -func (b *PulsarAdminNsIsolationPolicyToolBuilder) handleBrokerResource(client cmdutils.Client, operation, cluster string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminNsIsolationPolicyToolBuilder) handleBrokerResource(client cmdutils.Client, operation, cluster string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { switch operation { case "get": - name, err := request.RequireString("name") + name, err := adapter.RequireString(request, "name") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'name' for broker.get: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'name' for broker.get: %v", err), nil } // Get broker with policies brokerInfo, err := client.NsIsolationPolicy().GetBrokerWithNamespaceIsolationPolicy(cluster, name) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get broker with namespace isolation policy: %v", err)), nil + return adapter.NewErrorResult("Failed to get broker with namespace isolation policy: %v", err), nil } // Convert result to JSON string brokerInfoJSON, err := json.Marshal(brokerInfo) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to serialize broker information: %v", err)), nil + return adapter.NewErrorResult("Failed to serialize broker information: %v", err), nil } - return mcp.NewToolResultText(string(brokerInfoJSON)), nil + return adapter.NewTextResult(string(brokerInfoJSON)), nil default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid operation for resource 'broker': %s. Available operations: get", operation)), nil + return adapter.NewErrorResult("Invalid operation for resource 'broker': %s. Available operations: get", operation), nil } } // handleNsIsolationBrokersResource handles operations on the "brokers" resource for namespace isolation policies -func (b *PulsarAdminNsIsolationPolicyToolBuilder) handleNsIsolationBrokersResource(client cmdutils.Client, operation, cluster string, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminNsIsolationPolicyToolBuilder) handleNsIsolationBrokersResource(client cmdutils.Client, operation, cluster string, _ *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { switch operation { case "list": // Get all brokers with policies brokersInfo, err := client.NsIsolationPolicy().GetBrokersWithNamespaceIsolationPolicy(cluster) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get brokers with namespace isolation policy: %v", err)), nil + return adapter.NewErrorResult("Failed to get brokers with namespace isolation policy: %v", err), nil } // Convert result to JSON string brokersInfoJSON, err := json.Marshal(brokersInfo) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to serialize brokers information: %v", err)), nil + return adapter.NewErrorResult("Failed to serialize brokers information: %v", err), nil } - return mcp.NewToolResultText(string(brokersInfoJSON)), nil + return adapter.NewTextResult(string(brokersInfoJSON)), nil default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid operation for resource 'brokers': %s. Available operations: list", operation)), nil + return adapter.NewErrorResult("Invalid operation for resource 'brokers': %s. Available operations: list", operation), nil } } diff --git a/pkg/mcp/builders/pulsar/packages.go b/pkg/mcp/builders/pulsar/packages.go index 7d4d0f0..454986f 100644 --- a/pkg/mcp/builders/pulsar/packages.go +++ b/pkg/mcp/builders/pulsar/packages.go @@ -20,10 +20,10 @@ import ( "fmt" "strings" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/pulsarctl/pkg/cmdutils" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" mcpCtx "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" ) @@ -56,7 +56,7 @@ func NewPulsarAdminPackagesToolBuilder() *PulsarAdminPackagesToolBuilder { } // BuildTools builds the Pulsar admin packages tool list -func (b *PulsarAdminPackagesToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]server.ServerTool, error) { +func (b *PulsarAdminPackagesToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]builders.ServerTool, error) { // Check features - return empty list if no required features are present if !b.HasAnyRequiredFeature(config.Features) { return nil, nil @@ -71,7 +71,7 @@ func (b *PulsarAdminPackagesToolBuilder) BuildTools(_ context.Context, config bu tool := b.buildPackagesTool() handler := b.buildPackagesHandler(config.ReadOnly) - return []server.ServerTool{ + return []builders.ServerTool{ { Tool: tool, Handler: handler, @@ -80,7 +80,7 @@ func (b *PulsarAdminPackagesToolBuilder) BuildTools(_ context.Context, config bu } // buildPackagesTool builds the Pulsar admin packages MCP tool definition -func (b *PulsarAdminPackagesToolBuilder) buildPackagesTool() mcp.Tool { +func (b *PulsarAdminPackagesToolBuilder) buildPackagesTool() *mcpsdk.Tool { toolDesc := "Manage packages in Apache Pulsar. Support package scheme: `function://`, `source://`, `sink://`" + "Allows listing, viewing, updating, downloading and uploading packages. " + "Some operations require super-user permissions." @@ -97,51 +97,51 @@ func (b *PulsarAdminPackagesToolBuilder) buildPackagesTool() mcp.Tool { "- download: Download a package (requires super-user permissions)\n" + "- upload: Upload a package (requires super-user permissions)" - return mcp.NewTool("pulsar_admin_package", - mcp.WithDescription(toolDesc), - mcp.WithString("resource", mcp.Required(), - mcp.Description(resourceDesc), + return builders.NewTool("pulsar_admin_package", + builders.WithDescription(toolDesc), + builders.WithString("resource", builders.Required(), + builders.Description(resourceDesc), ), - mcp.WithString("operation", mcp.Required(), - mcp.Description(operationDesc), + builders.WithString("operation", builders.Required(), + builders.Description(operationDesc), ), - mcp.WithString("packageName", - mcp.Description("Name of the package to operate on. "+ + builders.WithString("packageName", + builders.Description("Name of the package to operate on. "+ "Required for operations on a specific package: get, update, delete, download, upload"), ), - mcp.WithString("namespace", - mcp.Description("The namespace name. Required for listing packages of a specific type"), + builders.WithString("namespace", + builders.Description("The namespace name. Required for listing packages of a specific type"), ), - mcp.WithString("type", - mcp.Description("Package type (function, source, sink). Required for listing packages of a specific type"), + builders.WithString("type", + builders.Description("Package type (function, source, sink). Required for listing packages of a specific type"), ), - mcp.WithString("description", - mcp.Description("Description of the package. Required for update and upload operations"), + builders.WithString("description", + builders.Description("Description of the package. Required for update and upload operations"), ), - mcp.WithString("contact", - mcp.Description("Contact information for the package. Optional for update and upload operations"), + builders.WithString("contact", + builders.Description("Contact information for the package. Optional for update and upload operations"), ), - mcp.WithString("path", - mcp.Description("Path to download a package to or upload a package from. Required for download and upload operations"), + builders.WithString("path", + builders.Description("Path to download a package to or upload a package from. Required for download and upload operations"), ), - mcp.WithObject("properties", - mcp.Description("Additional properties for the package as key-value pairs. Optional for update and upload operations"), + builders.WithObject("properties", + builders.Description("Additional properties for the package as key-value pairs. Optional for update and upload operations"), ), ) } // buildPackagesHandler builds the Pulsar admin packages handler function -func (b *PulsarAdminPackagesToolBuilder) buildPackagesHandler(readOnly bool) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminPackagesToolBuilder) buildPackagesHandler(readOnly bool) func(context.Context, *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - resource, err := request.RequireString("resource") + resource, err := adapter.RequireString(request, "resource") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get resource: %v", err)), nil + return adapter.NewErrorResult("Failed to get resource: %v", err), nil } - operation, err := request.RequireString("operation") + operation, err := adapter.RequireString(request, "operation") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get operation: %v", err)), nil + return adapter.NewErrorResult("Failed to get operation: %v", err), nil } // Normalize parameters @@ -150,18 +150,18 @@ func (b *PulsarAdminPackagesToolBuilder) buildPackagesHandler(readOnly bool) fun // Validate write operations in read-only mode if readOnly && (operation == "update" || operation == "delete" || operation == "download" || operation == "upload") { - return mcp.NewToolResultError("Write operations are not allowed in read-only mode"), nil + return adapter.NewErrorResult("Write operations are not allowed in read-only mode"), nil } // Get Pulsar session from context session := mcpCtx.GetPulsarSession(ctx) if session == nil { - return mcp.NewToolResultError("Pulsar session not found in context"), nil + return adapter.NewErrorResult("Pulsar session not found in context"), nil } client, err := session.GetAdminV3Client() if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get Pulsar client: %v", err)), nil + return adapter.NewErrorResult("Failed to get Pulsar client: %v", err), nil } // Dispatch based on resource type @@ -171,7 +171,7 @@ func (b *PulsarAdminPackagesToolBuilder) buildPackagesHandler(readOnly bool) fun case "packages": return b.handlePackagesResource(client, operation, request) default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid resource: %s. Available resources: package, packages", resource)), nil + return adapter.NewErrorResult("Invalid resource: %s. Available resources: package, packages", resource), nil } } } @@ -179,10 +179,10 @@ func (b *PulsarAdminPackagesToolBuilder) buildPackagesHandler(readOnly bool) fun // Helper functions // handlePackageResource handles operations on a specific package -func (b *PulsarAdminPackagesToolBuilder) handlePackageResource(client cmdutils.Client, operation string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - packageName, err := request.RequireString("packageName") +func (b *PulsarAdminPackagesToolBuilder) handlePackageResource(client cmdutils.Client, operation string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + packageName, err := adapter.RequireString(request, "packageName") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'packageName' for package operations: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'packageName' for package operations: %v", err), nil } switch operation { @@ -190,133 +190,135 @@ func (b *PulsarAdminPackagesToolBuilder) handlePackageResource(client cmdutils.C // Get package versions packageVersions, err := client.Packages().ListVersions(packageName) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list package versions: %v", err)), nil + return adapter.NewErrorResult("Failed to list package versions: %v", err), nil } // Convert result to JSON string packageVersionsJSON, err := json.Marshal(packageVersions) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to serialize package versions: %v", err)), nil + return adapter.NewErrorResult("Failed to serialize package versions: %v", err), nil } - return mcp.NewToolResultText(string(packageVersionsJSON)), nil + return adapter.NewTextResult(string(packageVersionsJSON)), nil case "get": // Get package metadata metadata, err := client.Packages().GetMetadata(packageName) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get package metadata: %v", err)), nil + return adapter.NewErrorResult("Failed to get package metadata: %v", err), nil } // Convert result to JSON string metadataJSON, err := json.Marshal(metadata) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to serialize package metadata: %v", err)), nil + return adapter.NewErrorResult("Failed to serialize package metadata: %v", err), nil } - return mcp.NewToolResultText(string(metadataJSON)), nil + return adapter.NewTextResult(string(metadataJSON)), nil case "update": - description, err := request.RequireString("description") + description, err := adapter.RequireString(request, "description") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'description' for package.update: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'description' for package.update: %v", err), nil } - contact := request.GetString("contact", "") - properties := b.extractProperties(request.GetArguments()) + contact := adapter.GetString(request, "contact", "") + args, _ := adapter.GetArgumentsMap(request) + properties := b.extractProperties(args) // Update package metadata err = client.Packages().UpdateMetadata(packageName, description, contact, properties) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to update package metadata: %v", err)), nil + return adapter.NewErrorResult("Failed to update package metadata: %v", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("The metadata of the package '%s' updated successfully", packageName)), nil + return adapter.NewTextResult(fmt.Sprintf("The metadata of the package '%s' updated successfully", packageName)), nil case "delete": // Delete package err = client.Packages().Delete(packageName) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to delete package: %v", err)), nil + return adapter.NewErrorResult("Failed to delete package: %v", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("The package '%s' deleted successfully", packageName)), nil + return adapter.NewTextResult(fmt.Sprintf("The package '%s' deleted successfully", packageName)), nil case "download": - path, err := request.RequireString("path") + path, err := adapter.RequireString(request, "path") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'path' for package.download: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'path' for package.download: %v", err), nil } // Download package err = client.Packages().Download(packageName, path) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to download package: %v", err)), nil + return adapter.NewErrorResult("Failed to download package: %v", err), nil } - return mcp.NewToolResultText( + return adapter.NewTextResult( fmt.Sprintf("The package '%s' downloaded to path '%s' successfully", packageName, path), ), nil case "upload": - path, err := request.RequireString("path") + path, err := adapter.RequireString(request, "path") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'path' for package.upload: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'path' for package.upload: %v", err), nil } - description, err := request.RequireString("description") + description, err := adapter.RequireString(request, "description") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'description' for package.upload: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'description' for package.upload: %v", err), nil } - contact := request.GetString("contact", "") - properties := b.extractProperties(request.GetArguments()) + contact := adapter.GetString(request, "contact", "") + args, _ := adapter.GetArgumentsMap(request) + properties := b.extractProperties(args) // Upload package err = client.Packages().Upload(packageName, path, description, contact, properties) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to upload package: %v", err)), nil + return adapter.NewErrorResult("Failed to upload package: %v", err), nil } - return mcp.NewToolResultText( + return adapter.NewTextResult( fmt.Sprintf("The package '%s' uploaded from path '%s' successfully", packageName, path), ), nil default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid operation for resource 'package': %s. Available operations: list, get, update, delete, download, upload", operation)), nil + return adapter.NewErrorResult("Invalid operation for resource 'package': %s. Available operations: list, get, update, delete, download, upload", operation), nil } } // handlePackagesResource handles operations on multiple packages -func (b *PulsarAdminPackagesToolBuilder) handlePackagesResource(client cmdutils.Client, operation string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminPackagesToolBuilder) handlePackagesResource(client cmdutils.Client, operation string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { switch operation { case "list": - packageType, err := request.RequireString("type") + packageType, err := adapter.RequireString(request, "type") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'type' for packages.list: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'type' for packages.list: %v", err), nil } - namespace, err := request.RequireString("namespace") + namespace, err := adapter.RequireString(request, "namespace") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'namespace' for packages.list: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'namespace' for packages.list: %v", err), nil } // Get package list packages, err := client.Packages().List(packageType, namespace) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list packages: %v", err)), nil + return adapter.NewErrorResult("Failed to list packages: %v", err), nil } // Convert result to JSON string packagesJSON, err := json.Marshal(packages) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to serialize package list: %v", err)), nil + return adapter.NewErrorResult("Failed to serialize package list: %v", err), nil } - return mcp.NewToolResultText(string(packagesJSON)), nil + return adapter.NewTextResult(string(packagesJSON)), nil default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid operation for resource 'packages': %s. Available operations: list", operation)), nil + return adapter.NewErrorResult("Invalid operation for resource 'packages': %s. Available operations: list", operation), nil } } diff --git a/pkg/mcp/builders/pulsar/produce.go b/pkg/mcp/builders/pulsar/produce.go index 402b24c..a194309 100644 --- a/pkg/mcp/builders/pulsar/produce.go +++ b/pkg/mcp/builders/pulsar/produce.go @@ -22,9 +22,9 @@ import ( "time" "github.com/apache/pulsar-client-go/pulsar" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" mcpCtx "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" ) @@ -58,7 +58,7 @@ func NewPulsarClientProduceToolBuilder() *PulsarClientProduceToolBuilder { // BuildTools builds the Pulsar Client Producer tool list // This is the core method implementing the ToolBuilder interface -func (b *PulsarClientProduceToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]server.ServerTool, error) { +func (b *PulsarClientProduceToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]builders.ServerTool, error) { // Check features - return empty list if no required features are present if !b.HasAnyRequiredFeature(config.Features) { return nil, nil @@ -73,7 +73,7 @@ func (b *PulsarClientProduceToolBuilder) BuildTools(_ context.Context, config bu tool := b.buildProduceTool() handler := b.buildProduceHandler(config.ReadOnly) - return []server.ServerTool{ + return []builders.ServerTool{ { Tool: tool, Handler: handler, @@ -83,7 +83,7 @@ func (b *PulsarClientProduceToolBuilder) BuildTools(_ context.Context, config bu // buildProduceTool builds the Pulsar Client Producer MCP tool definition // Migrated from the original tool definition logic -func (b *PulsarClientProduceToolBuilder) buildProduceTool() mcp.Tool { +func (b *PulsarClientProduceToolBuilder) buildProduceTool() *mcpsdk.Tool { toolDesc := "Produce messages to a Pulsar topic. " + "This tool allows you to send messages to a specified Pulsar topic with various options " + "to control message format, batching, rate limiting, and properties. " + @@ -93,61 +93,61 @@ func (b *PulsarClientProduceToolBuilder) buildProduceTool() mcp.Tool { "The tool supports message partitioning through keys and provides detailed feedback on sent messages. " + "Do not use this tool for Kafka protocol operations. Use 'kafka_client_produce' instead." - return mcp.NewTool("pulsar_client_produce", - mcp.WithDescription(toolDesc), - mcp.WithString("topic", mcp.Required(), - mcp.Description("The fully qualified topic name to produce to (format: [persistent|non-persistent]://tenant/namespace/topic). "+ + return builders.NewTool("pulsar_client_produce", + builders.WithDescription(toolDesc), + builders.WithString("topic", builders.Required(), + builders.Description("The fully qualified topic name to produce to (format: [persistent|non-persistent]://tenant/namespace/topic). "+ "For partitioned topics, messages will be distributed across partitions based on the partitioning scheme. "+ "If a message key is provided, messages with the same key will go to the same partition."), ), - mcp.WithArray("messages", - mcp.Description("List of message content to send. Each array element represents one message payload. "+ + builders.WithArray("messages", + builders.Description("List of message content to send. Each array element represents one message payload. "+ "IMPORTANT: Use this parameter to provide message content. Multiple messages can be sent in a single operation. "+ "Each message will be sent according to the specified num-produce parameter."), - mcp.Items( + builders.Items( map[string]interface{}{ "type": "string", "description": "Individual message content to be sent to the topic", }, ), ), - mcp.WithNumber("num-produce", - mcp.Description("Number of times to send the entire message set. "+ + builders.WithNumber("num-produce", + builders.Description("Number of times to send the entire message set. "+ "If you have 3 messages and set num-produce to 2, a total of 6 messages will be sent. (default: 1)"), ), - mcp.WithNumber("rate", - mcp.Description("Rate limiting in messages per second. Controls the maximum speed of message production. "+ + builders.WithNumber("rate", + builders.Description("Rate limiting in messages per second. Controls the maximum speed of message production. "+ "Set to 0 to produce messages as fast as possible without rate limiting. "+ "Higher rates may be limited by broker capacity and network bandwidth. (default: 0)"), ), - mcp.WithBoolean("disable-batching", - mcp.Description("Disable message batching. When false (default), Pulsar batches multiple messages "+ + builders.WithBoolean("disable-batching", + builders.Description("Disable message batching. When false (default), Pulsar batches multiple messages "+ "to improve throughput and reduce network overhead. Set to true to send each message individually. "+ "Disabling batching may reduce throughput but provides lower latency. (default: false)"), ), - mcp.WithBoolean("chunking", - mcp.Description("Enable message chunking for large messages. When true, messages larger than "+ + builders.WithBoolean("chunking", + builders.Description("Enable message chunking for large messages. When true, messages larger than "+ "the maximum allowed size will be automatically split into smaller chunks and reassembled on consumption. "+ "This allows sending messages that exceed broker size limits. (default: false)"), ), - mcp.WithString("separator", - mcp.Description("Character or string to split message content on. When specified, each message "+ + builders.WithString("separator", + builders.Description("Character or string to split message content on. When specified, each message "+ "in the messages array will be split by this separator to create additional individual messages. "+ "Useful for sending multiple messages from a single delimited string. (default: none)"), ), - mcp.WithArray("properties", - mcp.Description("Message properties in key=value format. Properties are metadata key-value pairs "+ + builders.WithArray("properties", + builders.Description("Message properties in key=value format. Properties are metadata key-value pairs "+ "attached to messages for filtering, routing, or application-specific processing. "+ "Example: ['priority=high', 'source=api', 'version=1.0']. Multiple properties can be specified."), - mcp.Items( + builders.Items( map[string]interface{}{ "type": "string", "description": "Property in key=value format", }, ), ), - mcp.WithString("key", - mcp.Description("Partitioning key for message routing. Messages with the same key will be sent "+ + builders.WithString("key", + builders.Description("Partitioning key for message routing. Messages with the same key will be sent "+ "to the same partition in partitioned topics, ensuring ordering for related messages. "+ "The key is also available to consumers for processing logic. Leave empty for round-robin partitioning."), ), @@ -156,40 +156,40 @@ func (b *PulsarClientProduceToolBuilder) buildProduceTool() mcp.Tool { // buildProduceHandler builds the Pulsar Client Producer handler function // Migrated from the original handler logic -func (b *PulsarClientProduceToolBuilder) buildProduceHandler(readOnly bool) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarClientProduceToolBuilder) buildProduceHandler(readOnly bool) func(context.Context, *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Check read-only mode - producing is a write operation if readOnly { - return mcp.NewToolResultError("Message production is not allowed in read-only mode"), nil + return adapter.NewErrorResult("Message production is not allowed in read-only mode"), nil } // Extract required parameters with validation - topic, err := request.RequireString("topic") + topic, err := adapter.RequireString(request, "topic") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get topic: %v", err)), nil + return adapter.NewErrorResult("Failed to get topic: %v", err), nil } // Set default values and extract optional parameters - messages := request.GetStringSlice("messages", []string{}) + messages := adapter.GetStringSlice(request, "messages", []string{}) if len(messages) == 0 { - return mcp.NewToolResultError("Please supply message content with 'messages' parameter"), nil + return adapter.NewErrorResult("Please supply message content with 'messages' parameter"), nil } - numProduce := int(request.GetFloat("num-produce", 1)) + numProduce := int(adapter.GetFloat(request, "num-produce", 1)) if numProduce < 1 { - return mcp.NewToolResultError("num-produce must be at least 1"), nil + return adapter.NewErrorResult("num-produce must be at least 1"), nil } - rate := request.GetFloat("rate", 0) + rate := adapter.GetFloat(request, "rate", 0) if rate < 0 { - return mcp.NewToolResultError("rate must be non-negative"), nil + return adapter.NewErrorResult("rate must be non-negative"), nil } - disableBatching := request.GetBool("disable-batching", false) - chunkingAllowed := request.GetBool("chunking", false) - separator := request.GetString("separator", "") - properties := request.GetStringSlice("properties", []string{}) - key := request.GetString("key", "") + disableBatching := adapter.GetBool(request, "disable-batching", false) + chunkingAllowed := adapter.GetBool(request, "chunking", false) + separator := adapter.GetString(request, "separator", "") + properties := adapter.GetStringSlice(request, "properties", []string{}) + key := adapter.GetString(request, "key", "") // Split messages by separator if needed if separator != "" && len(messages) > 0 { @@ -208,13 +208,13 @@ func (b *PulsarClientProduceToolBuilder) buildProduceHandler(readOnly bool) func // Get Pulsar session from context session := mcpCtx.GetPulsarSession(ctx) if session == nil { - return mcp.NewToolResultError("Pulsar session not found in context"), nil + return adapter.NewErrorResult("Pulsar session not found in context"), nil } // Setup client client, err := session.GetPulsarClient() if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to create Pulsar client: %v", err)), nil + return adapter.NewErrorResult("Failed to create Pulsar client: %v", err), nil } defer client.Close() @@ -233,20 +233,20 @@ func (b *PulsarClientProduceToolBuilder) buildProduceHandler(readOnly bool) func producer, err := client.CreateProducer(producerOpts) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to create producer: %v", err)), nil + return adapter.NewErrorResult("Failed to create producer: %v", err), nil } defer producer.Close() // Generate message bodies from messages messagePayloads, err := b.generateMessagePayloads(messages) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to generate message payloads: %v", err)), nil + return adapter.NewErrorResult("Failed to generate message payloads: %v", err), nil } // Parse properties propMap, err := b.parseProperties(properties) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to parse properties: %v", err)), nil + return adapter.NewErrorResult("Failed to parse properties: %v", err), nil } // Setup rate limiter @@ -286,7 +286,7 @@ func (b *PulsarClientProduceToolBuilder) buildProduceHandler(readOnly bool) func // Send the message msgID, err := producer.Send(ctx, msg) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to send message: %v", err)), nil + return adapter.NewErrorResult("Failed to send message: %v", err), nil } lastMessageID = msgID @@ -309,17 +309,17 @@ func (b *PulsarClientProduceToolBuilder) buildProduceHandler(readOnly bool) func // Unified error handling and utility functions // handleError provides unified error handling -func (b *PulsarClientProduceToolBuilder) handleError(operation string, err error) *mcp.CallToolResult { - return mcp.NewToolResultError(fmt.Sprintf("Failed to %s: %v", operation, err)) +func (b *PulsarClientProduceToolBuilder) handleError(operation string, err error) *mcpsdk.CallToolResult { + return adapter.NewErrorResult("Failed to %s: %v", operation, err) } // marshalResponse provides unified JSON serialization for responses -func (b *PulsarClientProduceToolBuilder) marshalResponse(data interface{}) (*mcp.CallToolResult, error) { +func (b *PulsarClientProduceToolBuilder) marshalResponse(data interface{}) (*mcpsdk.CallToolResult, error) { jsonBytes, err := json.Marshal(data) if err != nil { return b.handleError("marshal response", err), nil } - return mcp.NewToolResultText(string(jsonBytes)), nil + return adapter.NewTextResult(string(jsonBytes)), nil } // generateMessagePayloads generates message payloads from message strings diff --git a/pkg/mcp/builders/pulsar/resourcequotas.go b/pkg/mcp/builders/pulsar/resourcequotas.go index 7b53dec..20aaa7a 100644 --- a/pkg/mcp/builders/pulsar/resourcequotas.go +++ b/pkg/mcp/builders/pulsar/resourcequotas.go @@ -21,10 +21,10 @@ import ( "strings" "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/pulsarctl/pkg/cmdutils" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" mcpCtx "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" ) @@ -57,7 +57,7 @@ func NewPulsarAdminResourceQuotasToolBuilder() *PulsarAdminResourceQuotasToolBui } // BuildTools builds the Pulsar admin resource quotas tool list -func (b *PulsarAdminResourceQuotasToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]server.ServerTool, error) { +func (b *PulsarAdminResourceQuotasToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]builders.ServerTool, error) { // Check features - return empty list if no required features are present if !b.HasAnyRequiredFeature(config.Features) { return nil, nil @@ -72,7 +72,7 @@ func (b *PulsarAdminResourceQuotasToolBuilder) BuildTools(_ context.Context, con tool := b.buildResourceQuotasTool() handler := b.buildResourceQuotasHandler(config.ReadOnly) - return []server.ServerTool{ + return []builders.ServerTool{ { Tool: tool, Handler: handler, @@ -81,7 +81,7 @@ func (b *PulsarAdminResourceQuotasToolBuilder) BuildTools(_ context.Context, con } // buildResourceQuotasTool builds the Pulsar admin resource quotas MCP tool definition -func (b *PulsarAdminResourceQuotasToolBuilder) buildResourceQuotasTool() mcp.Tool { +func (b *PulsarAdminResourceQuotasToolBuilder) buildResourceQuotasTool() *mcpsdk.Tool { toolDesc := "Manage Apache Pulsar resource quotas for brokers, namespaces and bundles. " + "Resource quotas define limits for resource usage such as message rates, bandwidth, and memory. " + "These quotas help prevent resource abuse and ensure fair resource allocation across the Pulsar cluster. " + @@ -96,62 +96,62 @@ func (b *PulsarAdminResourceQuotasToolBuilder) buildResourceQuotasTool() mcp.Too "- set: Set the resource quota for a specified namespace bundle or default quota (requires super-user permissions)\n" + "- reset: Reset a namespace bundle's resource quota to default value (requires super-user permissions)" - return mcp.NewTool("pulsar_admin_resourcequota", - mcp.WithDescription(toolDesc), - mcp.WithString("resource", mcp.Required(), - mcp.Description(resourceDesc), + return builders.NewTool("pulsar_admin_resourcequota", + builders.WithDescription(toolDesc), + builders.WithString("resource", builders.Required(), + builders.Description(resourceDesc), ), - mcp.WithString("operation", mcp.Required(), - mcp.Description(operationDesc), + builders.WithString("operation", builders.Required(), + builders.Description(operationDesc), ), - mcp.WithString("namespace", - mcp.Description("The namespace name in the format 'tenant/namespace'. "+ + builders.WithString("namespace", + builders.Description("The namespace name in the format 'tenant/namespace'. "+ "Optional for 'get' and 'set' operations (to get/set default quota if omitted). "+ "Required for 'reset' operation."), ), - mcp.WithString("bundle", - mcp.Description("The bundle range in the format '{start-boundary}_{end-boundary}'. "+ + builders.WithString("bundle", + builders.Description("The bundle range in the format '{start-boundary}_{end-boundary}'. "+ "Must be specified together with namespace. Bundle is a hash range of the topic names belonging to a namespace."), ), - mcp.WithNumber("msgRateIn", - mcp.Description("Expected incoming messages per second. Required for 'set' operation. "+ + builders.WithNumber("msgRateIn", + builders.Description("Expected incoming messages per second. Required for 'set' operation. "+ "This defines the maximum rate of incoming messages allowed for the namespace or bundle."), ), - mcp.WithNumber("msgRateOut", - mcp.Description("Expected outgoing messages per second. Required for 'set' operation. "+ + builders.WithNumber("msgRateOut", + builders.Description("Expected outgoing messages per second. Required for 'set' operation. "+ "This defines the maximum rate of outgoing messages allowed for the namespace or bundle."), ), - mcp.WithNumber("bandwidthIn", - mcp.Description("Expected inbound bandwidth in bytes per second. Required for 'set' operation. "+ + builders.WithNumber("bandwidthIn", + builders.Description("Expected inbound bandwidth in bytes per second. Required for 'set' operation. "+ "This defines the maximum rate of incoming bytes allowed for the namespace or bundle."), ), - mcp.WithNumber("bandwidthOut", - mcp.Description("Expected outbound bandwidth in bytes per second. Required for 'set' operation. "+ + builders.WithNumber("bandwidthOut", + builders.Description("Expected outbound bandwidth in bytes per second. Required for 'set' operation. "+ "This defines the maximum rate of outgoing bytes allowed for the namespace or bundle."), ), - mcp.WithNumber("memory", - mcp.Description("Expected memory usage in Mbytes. Required for 'set' operation. "+ + builders.WithNumber("memory", + builders.Description("Expected memory usage in Mbytes. Required for 'set' operation. "+ "This defines the maximum memory allowed for storing messages for the namespace or bundle."), ), - mcp.WithBoolean("dynamic", - mcp.Description("Whether to allow quota to be dynamically re-calculated. Optional for 'set' operation. "+ + builders.WithBoolean("dynamic", + builders.Description("Whether to allow quota to be dynamically re-calculated. Optional for 'set' operation. "+ "If true, the broker can dynamically adjust the quota based on the current usage patterns."), ), ) } // buildResourceQuotasHandler builds the Pulsar admin resource quotas handler function -func (b *PulsarAdminResourceQuotasToolBuilder) buildResourceQuotasHandler(readOnly bool) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminResourceQuotasToolBuilder) buildResourceQuotasHandler(readOnly bool) func(context.Context, *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - resource, err := request.RequireString("resource") + resource, err := adapter.RequireString(request, "resource") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get resource: %v", err)), nil + return adapter.NewErrorResult("Failed to get resource: %v", err), nil } - operation, err := request.RequireString("operation") + operation, err := adapter.RequireString(request, "operation") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get operation: %v", err)), nil + return adapter.NewErrorResult("Failed to get operation: %v", err), nil } // Normalize parameters @@ -160,23 +160,23 @@ func (b *PulsarAdminResourceQuotasToolBuilder) buildResourceQuotasHandler(readOn // Validate write operations in read-only mode if readOnly && (operation == "set" || operation == "reset") { - return mcp.NewToolResultError("Write operations are not allowed in read-only mode"), nil + return adapter.NewErrorResult("Write operations are not allowed in read-only mode"), nil } // Verify resource type if resource != "quota" { - return mcp.NewToolResultError(fmt.Sprintf("Invalid resource: %s. Only 'quota' is supported", resource)), nil + return adapter.NewErrorResult("Invalid resource: %s. Only 'quota' is supported", resource), nil } // Get Pulsar session from context session := mcpCtx.GetPulsarSession(ctx) if session == nil { - return mcp.NewToolResultError("Pulsar session not found in context"), nil + return adapter.NewErrorResult("Pulsar session not found in context"), nil } admin, err := session.GetAdminClient() if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get admin client: %v", err)), nil + return adapter.NewErrorResult("Failed to get admin client: %v", err), nil } // Dispatch based on operation @@ -188,7 +188,7 @@ func (b *PulsarAdminResourceQuotasToolBuilder) buildResourceQuotasHandler(readOn case "reset": return b.handleQuotaReset(admin, request) default: - return mcp.NewToolResultError(fmt.Sprintf("Invalid operation: %s. Available operations: get, set, reset", operation)), nil + return adapter.NewErrorResult("Invalid operation: %s. Available operations: get, set, reset", operation), nil } } } @@ -196,14 +196,14 @@ func (b *PulsarAdminResourceQuotasToolBuilder) buildResourceQuotasHandler(readOn // Helper functions // handleQuotaGet handles getting a resource quota -func (b *PulsarAdminResourceQuotasToolBuilder) handleQuotaGet(admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminResourceQuotasToolBuilder) handleQuotaGet(admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get optional parameters - namespace := request.GetString("namespace", "") - bundle := request.GetString("bundle", "") + namespace := adapter.GetString(request, "namespace", "") + bundle := adapter.GetString(request, "bundle", "") // Check if both namespace and bundle are provided or neither is provided if (namespace != "" && bundle == "") || (namespace == "" && bundle != "") { - return mcp.NewToolResultError("When specifying a namespace, you must also specify a bundle and vice versa."), nil + return adapter.NewErrorResult("When specifying a namespace, you must also specify a bundle and vice versa."), nil } var ( @@ -215,17 +215,17 @@ func (b *PulsarAdminResourceQuotasToolBuilder) handleQuotaGet(admin cmdutils.Cli // Get default resource quota resourceQuotaData, getErr = admin.ResourceQuotas().GetDefaultResourceQuota() if getErr != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get default resource quota: %v", getErr)), nil + return adapter.NewErrorResult("Failed to get default resource quota: %v", getErr), nil } } else { // Get namespace bundle resource quota nsName, getErr := utils.GetNamespaceName(namespace) if getErr != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid namespace name '%s': %v", namespace, getErr)), nil + return adapter.NewErrorResult("Invalid namespace name '%s': %v", namespace, getErr), nil } resourceQuotaData, getErr = admin.ResourceQuotas().GetNamespaceBundleResourceQuota(nsName.String(), bundle) if getErr != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get resource quota for namespace '%s' bundle '%s': %v", + return adapter.NewErrorResult(fmt.Sprintf("Failed to get resource quota for namespace '%s' bundle '%s': %v", namespace, bundle, getErr)), nil } } @@ -233,48 +233,48 @@ func (b *PulsarAdminResourceQuotasToolBuilder) handleQuotaGet(admin cmdutils.Cli // Format the output jsonBytes, err := json.Marshal(resourceQuotaData) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal resource quota data: %v", err)), nil + return adapter.NewErrorResult("Failed to marshal resource quota data: %v", err), nil } - return mcp.NewToolResultText(string(jsonBytes)), nil + return adapter.NewTextResult(string(jsonBytes)), nil } // handleQuotaSet handles setting a resource quota -func (b *PulsarAdminResourceQuotasToolBuilder) handleQuotaSet(admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminResourceQuotasToolBuilder) handleQuotaSet(admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters for set operation - msgRateIn, err := request.RequireFloat("msgRateIn") + msgRateIn, err := adapter.RequireFloat(request, "msgRateIn") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'msgRateIn' for quota.set: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'msgRateIn' for quota.set: %v", err), nil } - msgRateOut, err := request.RequireFloat("msgRateOut") + msgRateOut, err := adapter.RequireFloat(request, "msgRateOut") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'msgRateOut' for quota.set: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'msgRateOut' for quota.set: %v", err), nil } - bandwidthIn, err := request.RequireFloat("bandwidthIn") + bandwidthIn, err := adapter.RequireFloat(request, "bandwidthIn") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'bandwidthIn' for quota.set: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'bandwidthIn' for quota.set: %v", err), nil } - bandwidthOut, err := request.RequireFloat("bandwidthOut") + bandwidthOut, err := adapter.RequireFloat(request, "bandwidthOut") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'bandwidthOut' for quota.set: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'bandwidthOut' for quota.set: %v", err), nil } - memory, err := request.RequireFloat("memory") + memory, err := adapter.RequireFloat(request, "memory") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'memory' for quota.set: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'memory' for quota.set: %v", err), nil } // Get optional parameters - namespace := request.GetString("namespace", "") - bundle := request.GetString("bundle", "") - dynamic := request.GetBool("dynamic", false) + namespace := adapter.GetString(request, "namespace", "") + bundle := adapter.GetString(request, "bundle", "") + dynamic := adapter.GetBool(request, "dynamic", false) // Check if both namespace and bundle are provided or neither is provided if (namespace != "" && bundle == "") || (namespace == "" && bundle != "") { - return mcp.NewToolResultError("When specifying a namespace, you must also specify a bundle and vice versa."), nil + return adapter.NewErrorResult("When specifying a namespace, you must also specify a bundle and vice versa."), nil } // Create resource quota object @@ -291,48 +291,48 @@ func (b *PulsarAdminResourceQuotasToolBuilder) handleQuotaSet(admin cmdutils.Cli // Set default resource quota err = admin.ResourceQuotas().SetDefaultResourceQuota(*quota) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to set default resource quota: %v", err)), nil + return adapter.NewErrorResult("Failed to set default resource quota: %v", err), nil } resultMsg = "Default resource quota set successfully" } else { // Set namespace bundle resource quota err = admin.ResourceQuotas().SetNamespaceBundleResourceQuota(namespace, bundle, *quota) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to set resource quota for namespace '%s' bundle '%s': %v", + return adapter.NewErrorResult(fmt.Sprintf("Failed to set resource quota for namespace '%s' bundle '%s': %v", namespace, bundle, err)), nil } resultMsg = fmt.Sprintf("Resource quota for namespace '%s' bundle '%s' set successfully", namespace, bundle) } - return mcp.NewToolResultText(resultMsg), nil + return adapter.NewTextResult(resultMsg), nil } // handleQuotaReset handles resetting a resource quota -func (b *PulsarAdminResourceQuotasToolBuilder) handleQuotaReset(admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminResourceQuotasToolBuilder) handleQuotaReset(admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters for reset operation - namespace, err := request.RequireString("namespace") + namespace, err := adapter.RequireString(request, "namespace") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'namespace' for quota.reset: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'namespace' for quota.reset: %v", err), nil } - bundle, err := request.RequireString("bundle") + bundle, err := adapter.RequireString(request, "bundle") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'bundle' for quota.reset: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'bundle' for quota.reset: %v", err), nil } // Parse namespace name nsName, err := utils.GetNamespaceName(namespace) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid namespace name '%s': %v", namespace, err)), nil + return adapter.NewErrorResult("Invalid namespace name '%s': %v", namespace, err), nil } // Reset namespace bundle resource quota err = admin.ResourceQuotas().ResetNamespaceBundleResourceQuota(nsName.String(), bundle) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to reset resource quota for namespace '%s' bundle '%s': %v", + return adapter.NewErrorResult(fmt.Sprintf("Failed to reset resource quota for namespace '%s' bundle '%s': %v", namespace, bundle, err)), nil } - return mcp.NewToolResultText(fmt.Sprintf("Resource quota for namespace '%s' bundle '%s' reset to default successfully", + return adapter.NewTextResult(fmt.Sprintf("Resource quota for namespace '%s' bundle '%s' reset to default successfully", namespace, bundle)), nil } diff --git a/pkg/mcp/builders/pulsar/schema.go b/pkg/mcp/builders/pulsar/schema.go index 8bf4de3..0cf9c8c 100644 --- a/pkg/mcp/builders/pulsar/schema.go +++ b/pkg/mcp/builders/pulsar/schema.go @@ -23,10 +23,10 @@ import ( "strings" "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/pulsarctl/pkg/cmdutils" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" mcpCtx "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" ) @@ -61,7 +61,7 @@ func NewPulsarAdminSchemaToolBuilder() *PulsarAdminSchemaToolBuilder { // BuildTools builds the Pulsar Admin Schema tool list // This is the core method implementing the ToolBuilder interface -func (b *PulsarAdminSchemaToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]server.ServerTool, error) { +func (b *PulsarAdminSchemaToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]builders.ServerTool, error) { // Check features - return empty list if no required features are present if !b.HasAnyRequiredFeature(config.Features) { return nil, nil @@ -76,7 +76,7 @@ func (b *PulsarAdminSchemaToolBuilder) BuildTools(_ context.Context, config buil tool := b.buildSchemaTool() handler := b.buildSchemaHandler(config.ReadOnly) - return []server.ServerTool{ + return []builders.ServerTool{ { Tool: tool, Handler: handler, @@ -86,7 +86,7 @@ func (b *PulsarAdminSchemaToolBuilder) BuildTools(_ context.Context, config buil // buildSchemaTool builds the Pulsar Admin Schema MCP tool definition // Migrated from the original tool definition logic -func (b *PulsarAdminSchemaToolBuilder) buildSchemaTool() mcp.Tool { +func (b *PulsarAdminSchemaToolBuilder) buildSchemaTool() *mcpsdk.Tool { toolDesc := "Manage Apache Pulsar schemas for topics. " + "Schemas in Pulsar define the structure of message data, enabling data validation, evolution, and interoperability. " + "Pulsar supports multiple schema types including AVRO, JSON, PROTOBUF, etc., allowing strong typing of message content. " + @@ -102,26 +102,26 @@ func (b *PulsarAdminSchemaToolBuilder) buildSchemaTool() mcp.Tool { "- upload: Upload a new schema for a topic (requires namespace admin permissions)\n" + "- delete: Delete the schema for a topic (requires namespace admin permissions)" - return mcp.NewTool("pulsar_admin_schema", - mcp.WithDescription(toolDesc), - mcp.WithString("resource", mcp.Required(), - mcp.Description(resourceDesc), + return builders.NewTool("pulsar_admin_schema", + builders.WithDescription(toolDesc), + builders.WithString("resource", builders.Required(), + builders.Description(resourceDesc), ), - mcp.WithString("operation", mcp.Required(), - mcp.Description(operationDesc), + builders.WithString("operation", builders.Required(), + builders.Description(operationDesc), ), - mcp.WithString("topic", mcp.Required(), - mcp.Description("The fully qualified topic name in the format 'persistent://tenant/namespace/topic'. "+ + builders.WithString("topic", builders.Required(), + builders.Description("The fully qualified topic name in the format 'persistent://tenant/namespace/topic'. "+ "A schema is always associated with a specific topic. The schema will be enforced for all producers "+ "and consumers of this topic."), ), - mcp.WithNumber("version", - mcp.Description("The schema version (optional for 'get' operation). "+ + builders.WithNumber("version", + builders.Description("The schema version (optional for 'get' operation). "+ "Pulsar maintains a versioned history of schemas. If not specified, the latest schema version will be returned. "+ "Use this parameter to retrieve a specific historical version of the schema."), ), - mcp.WithString("filename", - mcp.Description("The file path of the schema definition (required for 'upload' operation). "+ + builders.WithString("filename", + builders.Description("The file path of the schema definition (required for 'upload' operation). "+ "The file should contain a JSON object with 'type', 'schema', and optionally 'properties' fields. "+ "Supported schema types include: AVRO, JSON, PROTOBUF, PROTOBUF_NATIVE, KEY_VALUE, BYTES, STRING, INT8, INT16, INT32, INT64, FLOAT, DOUBLE, BOOLEAN, NONE."), ), @@ -130,22 +130,22 @@ func (b *PulsarAdminSchemaToolBuilder) buildSchemaTool() mcp.Tool { // buildSchemaHandler builds the Pulsar Admin Schema handler function // Migrated from the original handler logic -func (b *PulsarAdminSchemaToolBuilder) buildSchemaHandler(readOnly bool) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSchemaToolBuilder) buildSchemaHandler(readOnly bool) func(context.Context, *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - resource, err := request.RequireString("resource") + resource, err := adapter.RequireString(request, "resource") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get resource: %v", err)), nil + return adapter.NewErrorResult("Failed to get resource: %v", err), nil } - operation, err := request.RequireString("operation") + operation, err := adapter.RequireString(request, "operation") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get operation: %v", err)), nil + return adapter.NewErrorResult("Failed to get operation: %v", err), nil } - topic, err := request.RequireString("topic") + topic, err := adapter.RequireString(request, "topic") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'topic'. Please provide the fully qualified topic name: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'topic'. Please provide the fully qualified topic name: %v", err), nil } // Normalize parameters @@ -154,24 +154,24 @@ func (b *PulsarAdminSchemaToolBuilder) buildSchemaHandler(readOnly bool) func(co // Validate write operations in read-only mode if readOnly && (operation == "upload" || operation == "delete") { - return mcp.NewToolResultError("Write operations are not allowed in read-only mode"), nil + return adapter.NewErrorResult("Write operations are not allowed in read-only mode"), nil } // Verify resource type if resource != "schema" { - return mcp.NewToolResultError(fmt.Sprintf("Invalid resource: %s. Only 'schema' is supported", resource)), nil + return adapter.NewErrorResult("Invalid resource: %s. Only 'schema' is supported", resource), nil } // Get Pulsar session from context session := mcpCtx.GetPulsarSession(ctx) if session == nil { - return mcp.NewToolResultError("Pulsar session not found in context"), nil + return adapter.NewErrorResult("Pulsar session not found in context"), nil } // Create the admin client admin, err := session.GetAdminClient() if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get admin client: %v", err)), nil + return adapter.NewErrorResult("Failed to get admin client: %v", err), nil } // Dispatch based on operation @@ -183,7 +183,7 @@ func (b *PulsarAdminSchemaToolBuilder) buildSchemaHandler(readOnly bool) func(co case "delete": return b.handleSchemaDelete(admin, topic) default: - return mcp.NewToolResultError(fmt.Sprintf("Unknown operation: %s", operation)), nil + return adapter.NewErrorResult("Unknown operation: %s", operation), nil } } } @@ -200,30 +200,30 @@ func (b *PulsarAdminSchemaToolBuilder) prettyPrint(data []byte) ([]byte, error) // Operation handler functions - migrated from the original implementation // handleSchemaGet handles getting a schema -func (b *PulsarAdminSchemaToolBuilder) handleSchemaGet(admin cmdutils.Client, topic string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSchemaToolBuilder) handleSchemaGet(admin cmdutils.Client, topic string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get optional version parameter - version := request.GetFloat("version", 0) + version := adapter.GetFloat(request, "version", 0) // Get schema info if version != 0 { // Get schema by version info, err := admin.Schemas().GetSchemaInfoByVersion(topic, int64(version)) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get schema version %v for topic '%s': %v", + return adapter.NewErrorResult(fmt.Sprintf("Failed to get schema version %v for topic '%s': %v", version, topic, err)), nil } jsonBytes, err := json.Marshal(info) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to process schema information: %v", err)), nil + return adapter.NewErrorResult("Failed to process schema information: %v", err), nil } - return mcp.NewToolResultText(string(jsonBytes)), nil + return adapter.NewTextResult(string(jsonBytes)), nil } // Get latest schema schemaInfoWithVersion, err := admin.Schemas().GetSchemaInfoWithVersion(topic) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get latest schema for topic '%s': %v", + return adapter.NewErrorResult(fmt.Sprintf("Failed to get latest schema for topic '%s': %v", topic, err)), nil } @@ -231,66 +231,66 @@ func (b *PulsarAdminSchemaToolBuilder) handleSchemaGet(admin cmdutils.Client, to var output bytes.Buffer name, err := json.Marshal(schemaInfoWithVersion.SchemaInfo.Name) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to process schema name: %v", err)), nil + return adapter.NewErrorResult("Failed to process schema name: %v", err), nil } schemaType, err := json.Marshal(schemaInfoWithVersion.SchemaInfo.Type) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to process schema type: %v", err)), nil + return adapter.NewErrorResult("Failed to process schema type: %v", err), nil } properties, err := json.Marshal(schemaInfoWithVersion.SchemaInfo.Properties) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to process schema properties: %v", err)), nil + return adapter.NewErrorResult("Failed to process schema properties: %v", err), nil } schema, err := b.prettyPrint(schemaInfoWithVersion.SchemaInfo.Schema) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to format schema definition: %v", err)), nil + return adapter.NewErrorResult("Failed to format schema definition: %v", err), nil } fmt.Fprintf(&output, "{\n name: %s \n schema: %s\n type: %s \n properties: %s\n version: %d\n}", string(name), string(schema), string(schemaType), string(properties), schemaInfoWithVersion.Version) - return mcp.NewToolResultText(output.String()), nil + return adapter.NewTextResult(output.String()), nil } // handleSchemaUpload handles uploading a schema -func (b *PulsarAdminSchemaToolBuilder) handleSchemaUpload(admin cmdutils.Client, topic string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - filename, err := request.RequireString("filename") +func (b *PulsarAdminSchemaToolBuilder) handleSchemaUpload(admin cmdutils.Client, topic string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + filename, err := adapter.RequireString(request, "filename") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'filename' for schema.upload. Please provide the path to the schema definition file: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'filename' for schema.upload. Please provide the path to the schema definition file: %v", err), nil } // Read and parse the schema file var payload utils.PostSchemaPayload file, err := os.ReadFile(filename) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to read schema file '%s': %v", filename, err)), nil + return adapter.NewErrorResult("Failed to read schema file '%s': %v", filename, err), nil } err = json.Unmarshal(file, &payload) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to parse schema file '%s'. The file must contain valid JSON with 'type', 'schema', and optionally 'properties' fields: %v", + return adapter.NewErrorResult(fmt.Sprintf("Failed to parse schema file '%s'. The file must contain valid JSON with 'type', 'schema', and optionally 'properties' fields: %v", filename, err)), nil } // Upload the schema err = admin.Schemas().CreateSchemaByPayload(topic, payload) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to upload schema for topic '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Failed to upload schema for topic '%s': %v", topic, err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Schema uploaded successfully for topic '%s'", topic)), nil + return adapter.NewTextResult(fmt.Sprintf("Schema uploaded successfully for topic '%s'", topic)), nil } // handleSchemaDelete handles deleting a schema -func (b *PulsarAdminSchemaToolBuilder) handleSchemaDelete(admin cmdutils.Client, topic string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSchemaToolBuilder) handleSchemaDelete(admin cmdutils.Client, topic string) (*mcpsdk.CallToolResult, error) { // Delete the schema err := admin.Schemas().DeleteSchema(topic) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to delete schema for topic '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Failed to delete schema for topic '%s': %v", topic, err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Schema deleted successfully for topic '%s'", topic)), nil + return adapter.NewTextResult(fmt.Sprintf("Schema deleted successfully for topic '%s'", topic)), nil } diff --git a/pkg/mcp/builders/pulsar/sinks.go b/pkg/mcp/builders/pulsar/sinks.go index a120d11..1bddf9b 100644 --- a/pkg/mcp/builders/pulsar/sinks.go +++ b/pkg/mcp/builders/pulsar/sinks.go @@ -21,10 +21,10 @@ import ( "strings" "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/pulsarctl/pkg/cmdutils" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" mcpCtx "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" ) @@ -57,7 +57,7 @@ func NewPulsarAdminSinksToolBuilder() *PulsarAdminSinksToolBuilder { } // BuildTools builds the Pulsar admin sinks tool list -func (b *PulsarAdminSinksToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]server.ServerTool, error) { +func (b *PulsarAdminSinksToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]builders.ServerTool, error) { // Check features - return empty list if no required features are present if !b.HasAnyRequiredFeature(config.Features) { return nil, nil @@ -72,7 +72,7 @@ func (b *PulsarAdminSinksToolBuilder) BuildTools(_ context.Context, config build tool := b.buildSinksTool() handler := b.buildSinksHandler(config.ReadOnly) - return []server.ServerTool{ + return []builders.ServerTool{ { Tool: tool, Handler: handler, @@ -81,7 +81,7 @@ func (b *PulsarAdminSinksToolBuilder) BuildTools(_ context.Context, config build } // buildSinksTool builds the Pulsar admin sinks MCP tool definition -func (b *PulsarAdminSinksToolBuilder) buildSinksTool() mcp.Tool { +func (b *PulsarAdminSinksToolBuilder) buildSinksTool() *mcpsdk.Tool { toolDesc := "Manage Apache Pulsar Sinks for data movement and integration. " + "Pulsar Sinks are connectors that export data from Pulsar topics to external systems such as databases, " + "storage services, messaging systems, and third-party applications. " + @@ -105,66 +105,66 @@ func (b *PulsarAdminSinksToolBuilder) buildSinksTool() mcp.Tool { "- restart: Restart a sink\n" + "- list-built-in: List all built-in sink connectors available in the system" - return mcp.NewTool("pulsar_admin_sinks", - mcp.WithDescription(toolDesc), - mcp.WithString("operation", mcp.Required(), - mcp.Description(operationDesc)), - mcp.WithString("tenant", - mcp.Description("The tenant name. Tenants are the primary organizational unit in Pulsar, "+ + return builders.NewTool("pulsar_admin_sinks", + builders.WithDescription(toolDesc), + builders.WithString("operation", builders.Required(), + builders.Description(operationDesc)), + builders.WithString("tenant", + builders.Description("The tenant name. Tenants are the primary organizational unit in Pulsar, "+ "providing multi-tenancy and resource isolation. Sinks deployed within a tenant "+ "inherit its permissions and resource quotas. "+ "Required for all operations except 'list-built-in'.")), - mcp.WithString("namespace", - mcp.Description("The namespace name. Namespaces are logical groupings of topics and sinks "+ + builders.WithString("namespace", + builders.Description("The namespace name. Namespaces are logical groupings of topics and sinks "+ "within a tenant. They encapsulate configuration policies and access control. "+ "Sinks in a namespace typically process topics within the same namespace. "+ "Required for all operations except 'list-built-in'.")), - mcp.WithString("name", - mcp.Description("The sink name. Required for all operations except 'list' and 'list-built-in'. "+ + builders.WithString("name", + builders.Description("The sink name. Required for all operations except 'list' and 'list-built-in'. "+ "Names should be descriptive of the sink's purpose and must be unique within a namespace. "+ "Sink names are used in metrics, logs, and when addressing the sink via APIs.")), - mcp.WithString("archive", - mcp.Description("Path to the archive file containing the sink code. Optional for 'create' and 'update' operations. "+ + builders.WithString("archive", + builders.Description("Path to the archive file containing the sink code. Optional for 'create' and 'update' operations. "+ "Can be a local path, NAR file, or a URL accessible to the Pulsar broker. "+ "The archive should contain all dependencies for the sink connector. "+ "Either archive or sink-type must be specified, but not both.")), - mcp.WithString("sink-type", - mcp.Description("The built-in sink connector type to use. Optional for 'create' and 'update' operations. "+ + builders.WithString("sink-type", + builders.Description("The built-in sink connector type to use. Optional for 'create' and 'update' operations. "+ "Specifies which built-in connector to use, such as 'jdbc', 'elastic-search', 'kafka', etc. "+ "Use 'list-built-in' operation to see available sink types. "+ "Either sink-type or archive must be specified, but not both.")), - mcp.WithArray("inputs", - mcp.Description("The sink's input topics (array of strings). Optional for 'create' and 'update' operations. "+ + builders.WithArray("inputs", + builders.Description("The sink's input topics (array of strings). Optional for 'create' and 'update' operations. "+ "Topics must be specified in the format 'persistent://tenant/namespace/topic'. "+ "Sinks can consume from multiple topics, but they should have compatible schemas. "+ "All input topics should exist before the sink is created. "+ "Either inputs or topics-pattern must be specified."), - mcp.Items( + builders.Items( map[string]interface{}{ "type": "string", "description": "input topic", }, ), ), - mcp.WithString("topics-pattern", - mcp.Description("TopicsPattern to consume from list of topics that match the pattern. Optional for 'create' and 'update' operations. "+ + builders.WithString("topics-pattern", + builders.Description("TopicsPattern to consume from list of topics that match the pattern. Optional for 'create' and 'update' operations. "+ "Specified as a regular expression, e.g., 'persistent://tenant/namespace/prefix.*'. "+ "This allows the sink to automatically consume from topics that match the pattern, "+ "including topics created after the sink is deployed. "+ "Either topics-pattern or inputs must be specified.")), - mcp.WithString("subs-name", - mcp.Description("Pulsar subscription name for input topic consumer. Optional for 'create' and 'update' operations. "+ + builders.WithString("subs-name", + builders.Description("Pulsar subscription name for input topic consumer. Optional for 'create' and 'update' operations. "+ "Defines the subscription name used by the sink to consume from input topics. "+ "If not specified, a default subscription name will be generated. "+ "The subscription type used is Shared by default.")), - mcp.WithNumber("parallelism", - mcp.Description("The parallelism factor of the sink. Optional for 'create' and 'update' operations. "+ + builders.WithNumber("parallelism", + builders.Description("The parallelism factor of the sink. Optional for 'create' and 'update' operations. "+ "Determines how many instances of the sink will run concurrently. "+ "Higher values improve throughput but require more resources. "+ "Default is 1 (single instance). Recommended to align with topic partition count "+ "when consuming from partitioned topics.")), - mcp.WithObject("sink-config", - mcp.Description("User-defined sink config key/values. Optional for 'create' and 'update' operations. "+ + builders.WithObject("sink-config", + builders.Description("User-defined sink config key/values. Optional for 'create' and 'update' operations. "+ "Provides configuration parameters specific to the sink connector being used. "+ "For example, JDBC connection strings, Elasticsearch indices, S3 bucket details, etc. "+ "Specify as a JSON object with configuration properties required by the specific sink type. "+ @@ -173,12 +173,12 @@ func (b *PulsarAdminSinksToolBuilder) buildSinksTool() mcp.Tool { } // buildSinksHandler builds the Pulsar admin sinks handler function -func (b *PulsarAdminSinksToolBuilder) buildSinksHandler(readOnly bool) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSinksToolBuilder) buildSinksHandler(readOnly bool) func(context.Context, *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Extract and validate operation parameter - operation, err := request.RequireString("operation") + operation, err := adapter.RequireString(request, "operation") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'operation': %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'operation': %v", err), nil } // Check if the operation is valid @@ -188,7 +188,7 @@ func (b *PulsarAdminSinksToolBuilder) buildSinksHandler(readOnly bool) func(cont } if !validOperations[operation] { - return mcp.NewToolResultError(fmt.Sprintf("Invalid operation: '%s'. Supported operations: list, get, status, create, update, delete, start, stop, restart, list-built-in", operation)), nil + return adapter.NewErrorResult("Invalid operation: '%s'. Supported operations: list, get, status, create, update, delete, start, stop, restart, list-built-in", operation), nil } // Check write permissions for write operations @@ -198,18 +198,18 @@ func (b *PulsarAdminSinksToolBuilder) buildSinksHandler(readOnly bool) func(cont } if readOnly && writeOperations[operation] { - return mcp.NewToolResultError(fmt.Sprintf("Operation '%s' not allowed in read-only mode. Read-only mode restricts modifications to Pulsar Sinks.", operation)), nil + return adapter.NewErrorResult("Operation '%s' not allowed in read-only mode. Read-only mode restricts modifications to Pulsar Sinks.", operation), nil } // Get Pulsar session from context session := mcpCtx.GetPulsarSession(ctx) if session == nil { - return mcp.NewToolResultError("Pulsar session not found in context"), nil + return adapter.NewErrorResult("Pulsar session not found in context"), nil } admin, err := session.GetAdminV3Client() if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get Pulsar client: %v", err)), nil + return adapter.NewErrorResult("Failed to get Pulsar client: %v", err), nil } // List built-in sinks doesn't require tenant, namespace or name @@ -218,22 +218,22 @@ func (b *PulsarAdminSinksToolBuilder) buildSinksHandler(readOnly bool) func(cont } // Extract common parameters (all operations except list-built-in require tenant and namespace) - tenant, err := request.RequireString("tenant") + tenant, err := adapter.RequireString(request, "tenant") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'tenant': %v. A tenant is required for operation '%s'.", err, operation)), nil + return adapter.NewErrorResult("Missing required parameter 'tenant': %v. A tenant is required for operation '%s'.", err, operation), nil } - namespace, err := request.RequireString("namespace") + namespace, err := adapter.RequireString(request, "namespace") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'namespace': %v. A namespace is required for operation '%s'.", err, operation)), nil + return adapter.NewErrorResult("Missing required parameter 'namespace': %v. A namespace is required for operation '%s'.", err, operation), nil } // For all operations except 'list', name is required var name string if operation != "list" { - name, err = request.RequireString("name") + name, err = adapter.RequireString(request, "name") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'name' for operation '%s': %v. The sink name must be specified for this operation.", operation, err)), nil + return adapter.NewErrorResult("Missing required parameter 'name' for operation '%s': %v. The sink name must be specified for this operation.", operation, err), nil } } @@ -259,7 +259,7 @@ func (b *PulsarAdminSinksToolBuilder) buildSinksHandler(readOnly bool) func(cont return b.handleSinkRestart(ctx, admin, tenant, namespace, name) default: // This should never happen due to the valid operations check above - return mcp.NewToolResultError(fmt.Sprintf("Unsupported operation: %s", operation)), nil + return adapter.NewErrorResult("Unsupported operation: %s", operation), nil } } } @@ -267,71 +267,71 @@ func (b *PulsarAdminSinksToolBuilder) buildSinksHandler(readOnly bool) func(cont // Helper functions // handleSinkList handles listing all sinks under a namespace -func (b *PulsarAdminSinksToolBuilder) handleSinkList(_ context.Context, admin cmdutils.Client, tenant, namespace string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSinksToolBuilder) handleSinkList(_ context.Context, admin cmdutils.Client, tenant, namespace string) (*mcpsdk.CallToolResult, error) { sinks, err := admin.Sinks().ListSinks(tenant, namespace) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list sinks in tenant '%s' namespace '%s': %v. Check that the tenant and namespace exist and you have proper permissions.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to list sinks in tenant '%s' namespace '%s': %v. Check that the tenant and namespace exist and you have proper permissions.", tenant, namespace, err)), nil } // Convert result to JSON string sinksJSON, err := json.Marshal(sinks) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to serialize sink list: %v", err)), nil + return adapter.NewErrorResult("Failed to serialize sink list: %v", err), nil } - return mcp.NewToolResultText(string(sinksJSON)), nil + return adapter.NewTextResult(string(sinksJSON)), nil } // handleSinkGet handles getting information about a sink -func (b *PulsarAdminSinksToolBuilder) handleSinkGet(_ context.Context, admin cmdutils.Client, tenant, namespace, name string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSinksToolBuilder) handleSinkGet(_ context.Context, admin cmdutils.Client, tenant, namespace, name string) (*mcpsdk.CallToolResult, error) { sink, err := admin.Sinks().GetSink(tenant, namespace, name) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get sink '%s' in tenant '%s' namespace '%s': %v. Verify the sink exists and you have proper permissions.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to get sink '%s' in tenant '%s' namespace '%s': %v. Verify the sink exists and you have proper permissions.", name, tenant, namespace, err)), nil } // Convert result to JSON string sinkJSON, err := json.Marshal(sink) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to serialize sink info: %v", err)), nil + return adapter.NewErrorResult("Failed to serialize sink info: %v", err), nil } - return mcp.NewToolResultText(string(sinkJSON)), nil + return adapter.NewTextResult(string(sinkJSON)), nil } // handleSinkStatus handles getting the status of a sink -func (b *PulsarAdminSinksToolBuilder) handleSinkStatus(_ context.Context, admin cmdutils.Client, tenant, namespace, name string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSinksToolBuilder) handleSinkStatus(_ context.Context, admin cmdutils.Client, tenant, namespace, name string) (*mcpsdk.CallToolResult, error) { status, err := admin.Sinks().GetSinkStatus(tenant, namespace, name) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get status for sink '%s' in tenant '%s' namespace '%s': %v. Verify the sink exists and is properly deployed.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to get status for sink '%s' in tenant '%s' namespace '%s': %v. Verify the sink exists and is properly deployed.", name, tenant, namespace, err)), nil } // Convert result to JSON string statusJSON, err := json.Marshal(status) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to serialize sink status: %v", err)), nil + return adapter.NewErrorResult("Failed to serialize sink status: %v", err), nil } - return mcp.NewToolResultText(string(statusJSON)), nil + return adapter.NewTextResult(string(statusJSON)), nil } // handleSinkCreate handles creating a new sink -func (b *PulsarAdminSinksToolBuilder) handleSinkCreate(_ context.Context, admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - tenant, err := request.RequireString("tenant") +func (b *PulsarAdminSinksToolBuilder) handleSinkCreate(_ context.Context, admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + tenant, err := adapter.RequireString(request, "tenant") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get tenant: %v", err)), nil + return adapter.NewErrorResult("Failed to get tenant: %v", err), nil } - namespace, err := request.RequireString("namespace") + namespace, err := adapter.RequireString(request, "namespace") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get namespace: %v", err)), nil + return adapter.NewErrorResult("Failed to get namespace: %v", err), nil } - name, err := request.RequireString("name") + name, err := adapter.RequireString(request, "name") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get name: %v", err)), nil + return adapter.NewErrorResult("Failed to get name: %v", err), nil } // Create a new SinkData object @@ -343,68 +343,64 @@ func (b *PulsarAdminSinksToolBuilder) handleSinkCreate(_ context.Context, admin } // Get optional parameters - archive := request.GetString("archive", "") + archive := adapter.GetString(request, "archive", "") if archive != "" { sinkData.Archive = archive } - sinkType := request.GetString("sink-type", "") + sinkType := adapter.GetString(request, "sink-type", "") if sinkType != "" { sinkData.SinkType = sinkType } - inputsArray := request.GetStringSlice("inputs", []string{}) + inputsArray := adapter.GetStringSlice(request, "inputs", []string{}) if len(inputsArray) > 0 { sinkData.Inputs = strings.Join(inputsArray, ",") } - topicsPattern := request.GetString("topics-pattern", "") + topicsPattern := adapter.GetString(request, "topics-pattern", "") if topicsPattern != "" { sinkData.TopicsPattern = topicsPattern } - subsName := request.GetString("subs-name", "") + subsName := adapter.GetString(request, "subs-name", "") if subsName != "" { sinkData.SubsName = subsName } - parallelismFloat := request.GetFloat("parallelism", 1) + parallelismFloat := adapter.GetFloat(request, "parallelism", 1) if parallelismFloat >= 0 { sinkData.Parallelism = int(parallelismFloat) } // Get sink config if available - var sinkConfigMap map[string]interface{} - sinkConfigObj, ok := request.GetArguments()["sink-config"] - if ok && sinkConfigObj != nil { - if configMap, isMap := sinkConfigObj.(map[string]interface{}); isMap { - sinkConfigMap = configMap - // Convert to JSON string - sinkConfigJSON, err := json.Marshal(sinkConfigMap) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal sink-config: %v. Ensure the sink configuration is a valid JSON object.", err)), nil - } - sinkData.SinkConfigString = string(sinkConfigJSON) + sinkConfigMap := adapter.GetObject(request, "sink-config") + if sinkConfigMap != nil { + // Convert to JSON string + sinkConfigJSON, err := json.Marshal(sinkConfigMap) + if err != nil { + return adapter.NewErrorResult("Failed to marshal sink-config: %v. Ensure the sink configuration is a valid JSON object.", err), nil } + sinkData.SinkConfigString = string(sinkConfigJSON) } // Validate inputs if sinkData.Archive == "" && sinkData.SinkType == "" { - return mcp.NewToolResultError("Missing required parameter: Either 'archive' or 'sink-type' must be specified for sink creation. Use 'archive' for custom connectors or 'sink-type' for built-in connectors."), nil + return adapter.NewErrorResult("Missing required parameter: Either 'archive' or 'sink-type' must be specified for sink creation. Use 'archive' for custom connectors or 'sink-type' for built-in connectors."), nil } if sinkData.Archive != "" && sinkData.SinkType != "" { - return mcp.NewToolResultError("Invalid parameters: Cannot specify both 'archive' and 'sink-type'. Use only one of these parameters based on your connector type."), nil + return adapter.NewErrorResult("Invalid parameters: Cannot specify both 'archive' and 'sink-type'. Use only one of these parameters based on your connector type."), nil } if sinkData.Inputs == "" && sinkData.TopicsPattern == "" { - return mcp.NewToolResultError("Missing required parameter: Either 'inputs' or 'topics-pattern' must be specified. The sink needs a source of data to consume from Pulsar."), nil + return adapter.NewErrorResult("Missing required parameter: Either 'inputs' or 'topics-pattern' must be specified. The sink needs a source of data to consume from Pulsar."), nil } // Process the arguments err = b.processArguments(sinkData) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to process arguments: %v", err)), nil + return adapter.NewErrorResult("Failed to process arguments: %v", err), nil } // Create the sink @@ -415,29 +411,29 @@ func (b *PulsarAdminSinksToolBuilder) handleSinkCreate(_ context.Context, admin } if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to create sink '%s' in tenant '%s' namespace '%s': %v. Verify all parameters are correct and required resources exist.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to create sink '%s' in tenant '%s' namespace '%s': %v. Verify all parameters are correct and required resources exist.", name, tenant, namespace, err)), nil } - return mcp.NewToolResultText(fmt.Sprintf("Created sink '%s' successfully in tenant '%s' namespace '%s'. The sink will start consuming from its input topics and writing to the configured destination.", + return adapter.NewTextResult(fmt.Sprintf("Created sink '%s' successfully in tenant '%s' namespace '%s'. The sink will start consuming from its input topics and writing to the configured destination.", name, tenant, namespace)), nil } // handleSinkUpdate handles updating an existing sink -func (b *PulsarAdminSinksToolBuilder) handleSinkUpdate(_ context.Context, admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - tenant, err := request.RequireString("tenant") +func (b *PulsarAdminSinksToolBuilder) handleSinkUpdate(_ context.Context, admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + tenant, err := adapter.RequireString(request, "tenant") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get tenant: %v", err)), nil + return adapter.NewErrorResult("Failed to get tenant: %v", err), nil } - namespace, err := request.RequireString("namespace") + namespace, err := adapter.RequireString(request, "namespace") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get namespace: %v", err)), nil + return adapter.NewErrorResult("Failed to get namespace: %v", err), nil } - name, err := request.RequireString("name") + name, err := adapter.RequireString(request, "name") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get name: %v", err)), nil + return adapter.NewErrorResult("Failed to get name: %v", err), nil } // Create a new SinkData object @@ -449,60 +445,56 @@ func (b *PulsarAdminSinksToolBuilder) handleSinkUpdate(_ context.Context, admin } // Get optional parameters - archive := request.GetString("archive", "") + archive := adapter.GetString(request, "archive", "") if archive != "" { sinkData.Archive = archive } - sinkType := request.GetString("sink-type", "") + sinkType := adapter.GetString(request, "sink-type", "") if sinkType != "" { sinkData.SinkType = sinkType } - inputsArray := request.GetStringSlice("inputs", []string{}) + inputsArray := adapter.GetStringSlice(request, "inputs", []string{}) if len(inputsArray) > 0 { sinkData.Inputs = strings.Join(inputsArray, ",") } - topicsPattern := request.GetString("topics-pattern", "") + topicsPattern := adapter.GetString(request, "topics-pattern", "") if topicsPattern != "" { sinkData.TopicsPattern = topicsPattern } - subsName := request.GetString("subs-name", "") + subsName := adapter.GetString(request, "subs-name", "") if subsName != "" { sinkData.SubsName = subsName } - parallelismFloat := request.GetFloat("parallelism", 1) + parallelismFloat := adapter.GetFloat(request, "parallelism", 1) if parallelismFloat >= 0 { sinkData.Parallelism = int(parallelismFloat) } // Get sink config if available - var sinkConfigMap map[string]interface{} - sinkConfigObj, ok := request.GetArguments()["sink-config"] - if ok && sinkConfigObj != nil { - if configMap, isMap := sinkConfigObj.(map[string]interface{}); isMap { - sinkConfigMap = configMap - // Convert to JSON string - sinkConfigJSON, err := json.Marshal(sinkConfigMap) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal sink-config: %v. Ensure the sink configuration is a valid JSON object.", err)), nil - } - sinkData.SinkConfigString = string(sinkConfigJSON) + sinkConfigMap := adapter.GetObject(request, "sink-config") + if sinkConfigMap != nil { + // Convert to JSON string + sinkConfigJSON, err := json.Marshal(sinkConfigMap) + if err != nil { + return adapter.NewErrorResult("Failed to marshal sink-config: %v. Ensure the sink configuration is a valid JSON object.", err), nil } + sinkData.SinkConfigString = string(sinkConfigJSON) } // Validate inputs if both are specified if sinkData.Archive != "" && sinkData.SinkType != "" { - return mcp.NewToolResultError("Invalid parameters: Cannot specify both 'archive' and 'sink-type'. Use only one of these parameters based on your connector type."), nil + return adapter.NewErrorResult("Invalid parameters: Cannot specify both 'archive' and 'sink-type'. Use only one of these parameters based on your connector type."), nil } // Process the arguments err = b.processArguments(sinkData) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to process arguments: %v", err)), nil + return adapter.NewErrorResult("Failed to process arguments: %v", err), nil } // Create update options @@ -518,76 +510,76 @@ func (b *PulsarAdminSinksToolBuilder) handleSinkUpdate(_ context.Context, admin } if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to update sink '%s' in tenant '%s' namespace '%s': %v. Verify the sink exists and all parameters are valid.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to update sink '%s' in tenant '%s' namespace '%s': %v. Verify the sink exists and all parameters are valid.", name, tenant, namespace, err)), nil } - return mcp.NewToolResultText(fmt.Sprintf("Updated sink '%s' successfully in tenant '%s' namespace '%s'. The sink may need to be restarted to apply all changes.", + return adapter.NewTextResult(fmt.Sprintf("Updated sink '%s' successfully in tenant '%s' namespace '%s'. The sink may need to be restarted to apply all changes.", name, tenant, namespace)), nil } // handleSinkDelete handles deleting a sink -func (b *PulsarAdminSinksToolBuilder) handleSinkDelete(_ context.Context, admin cmdutils.Client, tenant, namespace, name string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSinksToolBuilder) handleSinkDelete(_ context.Context, admin cmdutils.Client, tenant, namespace, name string) (*mcpsdk.CallToolResult, error) { err := admin.Sinks().DeleteSink(tenant, namespace, name) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to delete sink '%s' in tenant '%s' namespace '%s': %v. Verify the sink exists and you have deletion permissions.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to delete sink '%s' in tenant '%s' namespace '%s': %v. Verify the sink exists and you have deletion permissions.", name, tenant, namespace, err)), nil } - return mcp.NewToolResultText(fmt.Sprintf("Deleted sink '%s' successfully from tenant '%s' namespace '%s'. All running instances have been terminated.", + return adapter.NewTextResult(fmt.Sprintf("Deleted sink '%s' successfully from tenant '%s' namespace '%s'. All running instances have been terminated.", name, tenant, namespace)), nil } // handleSinkStart handles starting a sink -func (b *PulsarAdminSinksToolBuilder) handleSinkStart(_ context.Context, admin cmdutils.Client, tenant, namespace, name string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSinksToolBuilder) handleSinkStart(_ context.Context, admin cmdutils.Client, tenant, namespace, name string) (*mcpsdk.CallToolResult, error) { err := admin.Sinks().StartSink(tenant, namespace, name) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to start sink '%s' in tenant '%s' namespace '%s': %v. Verify the sink exists and is not already running.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to start sink '%s' in tenant '%s' namespace '%s': %v. Verify the sink exists and is not already running.", name, tenant, namespace, err)), nil } - return mcp.NewToolResultText(fmt.Sprintf("Started sink '%s' successfully in tenant '%s' namespace '%s'. The sink will begin consuming from its input topics.", + return adapter.NewTextResult(fmt.Sprintf("Started sink '%s' successfully in tenant '%s' namespace '%s'. The sink will begin consuming from its input topics.", name, tenant, namespace)), nil } // handleSinkStop handles stopping a sink -func (b *PulsarAdminSinksToolBuilder) handleSinkStop(_ context.Context, admin cmdutils.Client, tenant, namespace, name string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSinksToolBuilder) handleSinkStop(_ context.Context, admin cmdutils.Client, tenant, namespace, name string) (*mcpsdk.CallToolResult, error) { err := admin.Sinks().StopSink(tenant, namespace, name) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to stop sink '%s' in tenant '%s' namespace '%s': %v. Verify the sink exists and is currently running.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to stop sink '%s' in tenant '%s' namespace '%s': %v. Verify the sink exists and is currently running.", name, tenant, namespace, err)), nil } - return mcp.NewToolResultText(fmt.Sprintf("Stopped sink '%s' successfully in tenant '%s' namespace '%s'. The sink will no longer consume messages until restarted.", + return adapter.NewTextResult(fmt.Sprintf("Stopped sink '%s' successfully in tenant '%s' namespace '%s'. The sink will no longer consume messages until restarted.", name, tenant, namespace)), nil } // handleSinkRestart handles restarting a sink -func (b *PulsarAdminSinksToolBuilder) handleSinkRestart(_ context.Context, admin cmdutils.Client, tenant, namespace, name string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSinksToolBuilder) handleSinkRestart(_ context.Context, admin cmdutils.Client, tenant, namespace, name string) (*mcpsdk.CallToolResult, error) { err := admin.Sinks().RestartSink(tenant, namespace, name) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to restart sink '%s' in tenant '%s' namespace '%s': %v. Verify the sink exists and is properly deployed.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to restart sink '%s' in tenant '%s' namespace '%s': %v. Verify the sink exists and is properly deployed.", name, tenant, namespace, err)), nil } - return mcp.NewToolResultText(fmt.Sprintf("Restarted sink '%s' successfully in tenant '%s' namespace '%s'. All sink instances have been restarted.", + return adapter.NewTextResult(fmt.Sprintf("Restarted sink '%s' successfully in tenant '%s' namespace '%s'. All sink instances have been restarted.", name, tenant, namespace)), nil } // handleListBuiltInSinks handles listing all built-in sink connectors -func (b *PulsarAdminSinksToolBuilder) handleListBuiltInSinks(_ context.Context, admin cmdutils.Client) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSinksToolBuilder) handleListBuiltInSinks(_ context.Context, admin cmdutils.Client) (*mcpsdk.CallToolResult, error) { sinks, err := admin.Sinks().GetBuiltInSinks() if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list built-in sinks: %v. There might be an issue connecting to the Pulsar cluster.", err)), nil + return adapter.NewErrorResult("Failed to list built-in sinks: %v. There might be an issue connecting to the Pulsar cluster.", err), nil } // Convert result to JSON string sinksJSON, err := json.Marshal(sinks) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to serialize built-in sinks: %v", err)), nil + return adapter.NewErrorResult("Failed to serialize built-in sinks: %v", err), nil } - return mcp.NewToolResultText(string(sinksJSON)), nil + return adapter.NewTextResult(string(sinksJSON)), nil } // processArguments is a simplified version of the pulsarctl function to process sink arguments diff --git a/pkg/mcp/builders/pulsar/sources.go b/pkg/mcp/builders/pulsar/sources.go index ca9d148..8a87320 100644 --- a/pkg/mcp/builders/pulsar/sources.go +++ b/pkg/mcp/builders/pulsar/sources.go @@ -21,10 +21,10 @@ import ( "strings" "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/pulsarctl/pkg/cmdutils" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" mcpCtx "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" ) @@ -57,7 +57,7 @@ func NewPulsarAdminSourcesToolBuilder() *PulsarAdminSourcesToolBuilder { } // BuildTools builds the Pulsar admin sources tool list -func (b *PulsarAdminSourcesToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]server.ServerTool, error) { +func (b *PulsarAdminSourcesToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]builders.ServerTool, error) { // Check features - return empty list if no required features are present if !b.HasAnyRequiredFeature(config.Features) { return nil, nil @@ -72,7 +72,7 @@ func (b *PulsarAdminSourcesToolBuilder) BuildTools(_ context.Context, config bui tool := b.buildSourcesTool() handler := b.buildSourcesHandler(config.ReadOnly) - return []server.ServerTool{ + return []builders.ServerTool{ { Tool: tool, Handler: handler, @@ -81,7 +81,7 @@ func (b *PulsarAdminSourcesToolBuilder) BuildTools(_ context.Context, config bui } // buildSourcesTool builds the Pulsar admin sources MCP tool definition -func (b *PulsarAdminSourcesToolBuilder) buildSourcesTool() mcp.Tool { +func (b *PulsarAdminSourcesToolBuilder) buildSourcesTool() *mcpsdk.Tool { toolDesc := "Manage Apache Pulsar Sources for data ingestion and integration. " + "Pulsar Sources are connectors that import data from external systems into Pulsar topics. " + "Sources connect to external systems such as databases, messaging platforms, storage services, " + @@ -104,68 +104,68 @@ func (b *PulsarAdminSourcesToolBuilder) buildSourcesTool() mcp.Tool { "- restart: Restart a source\n" + "- list-built-in: List all built-in source connectors available in the system" - return mcp.NewTool("pulsar_admin_sources", - mcp.WithDescription(toolDesc), - mcp.WithString("operation", mcp.Required(), - mcp.Description(operationDesc)), - mcp.WithString("tenant", - mcp.Description("The tenant name. Tenants are the primary organizational unit in Pulsar, "+ + return builders.NewTool("pulsar_admin_sources", + builders.WithDescription(toolDesc), + builders.WithString("operation", builders.Required(), + builders.Description(operationDesc)), + builders.WithString("tenant", + builders.Description("The tenant name. Tenants are the primary organizational unit in Pulsar, "+ "providing multi-tenancy and resource isolation. Sources deployed within a tenant "+ "inherit its permissions and resource quotas. "+ "Required for all operations except 'list-built-in'.")), - mcp.WithString("namespace", - mcp.Description("The namespace name. Namespaces are logical groupings of topics and sources "+ + builders.WithString("namespace", + builders.Description("The namespace name. Namespaces are logical groupings of topics and sources "+ "within a tenant. They encapsulate configuration policies and access control. "+ "Sources in a namespace typically publish to topics within the same namespace. "+ "Required for all operations except 'list-built-in'.")), - mcp.WithString("name", - mcp.Description("The source name. Required for all operations except 'list' and 'list-built-in'. "+ + builders.WithString("name", + builders.Description("The source name. Required for all operations except 'list' and 'list-built-in'. "+ "Names should be descriptive of the source's purpose and must be unique within a namespace. "+ "Source names are used in metrics, logs, and when addressing the source via APIs.")), - mcp.WithString("archive", - mcp.Description("Path to the archive file containing the source code. Optional for 'create' and 'update' operations. "+ + builders.WithString("archive", + builders.Description("Path to the archive file containing the source code. Optional for 'create' and 'update' operations. "+ "Can be a local path, NAR file, or a URL accessible to the Pulsar broker. "+ "The archive should contain all dependencies for the source connector. "+ "Either archive or source-type must be specified, but not both.")), - mcp.WithString("source-type", - mcp.Description("The built-in source connector type to use. Optional for 'create' and 'update' operations. "+ + builders.WithString("source-type", + builders.Description("The built-in source connector type to use. Optional for 'create' and 'update' operations. "+ "Specifies which built-in connector to use, such as 'kafka', 'jdbc', 'file', etc. "+ "Use 'list-built-in' operation to see available source types. "+ "Either source-type or archive must be specified, but not both.")), - mcp.WithString("destination-topic-name", - mcp.Description("The Pulsar topic to which data is published. Required for 'create' operation, optional for 'update'. "+ + builders.WithString("destination-topic-name", + builders.Description("The Pulsar topic to which data is published. Required for 'create' operation, optional for 'update'. "+ "Specified in the format 'persistent://tenant/namespace/topic'. "+ "This is the topic where the source will send the data it extracts from the external system. "+ "The topic will be automatically created if it doesn't exist.")), - mcp.WithString("deserialization-classname", - mcp.Description("The SerDe (Serialization/Deserialization) classname for the source. Optional for 'create' and 'update'. "+ + builders.WithString("deserialization-classname", + builders.Description("The SerDe (Serialization/Deserialization) classname for the source. Optional for 'create' and 'update'. "+ "Specifies how to convert data from the external system into Pulsar messages. "+ "Common SerDe classes include AvroSchema, JsonSchema, StringSchema, etc. "+ "If not specified, the source will use the default SerDe for the connector type.")), - mcp.WithString("schema-type", - mcp.Description("The schema type to be used to encode messages emitted from the source. Optional for 'create' and 'update'. "+ + builders.WithString("schema-type", + builders.Description("The schema type to be used to encode messages emitted from the source. Optional for 'create' and 'update'. "+ "Available schema types include: 'avro', 'json', 'protobuf', 'string', etc. "+ "Schema types ensure data compatibility and enable schema evolution. "+ "The schema type should match the format of data being ingested.")), - mcp.WithString("classname", - mcp.Description("The source's class name if archive is a file-url-path (file://...). Optional for 'create' and 'update'. "+ + builders.WithString("classname", + builders.Description("The source's class name if archive is a file-url-path (file://...). Optional for 'create' and 'update'. "+ "This specifies the fully qualified class name that implements the source connector. "+ "Only needed when using a custom source implementation in a JAR file. "+ "Built-in connectors don't require this parameter.")), - mcp.WithString("processing-guarantees", - mcp.Description("The processing guarantees (delivery semantics) applied to the source. Optional for 'create' and 'update'. "+ + builders.WithString("processing-guarantees", + builders.Description("The processing guarantees (delivery semantics) applied to the source. Optional for 'create' and 'update'. "+ "Available options: 'atleast_once', 'atmost_once', 'effectively_once'. "+ "Controls how data is delivered in failure scenarios. "+ "'atleast_once' is the most common and ensures no data loss but may have duplicates. "+ "Default is 'atleast_once'.")), - mcp.WithNumber("parallelism", - mcp.Description("The parallelism factor of the source. Optional for 'create' and 'update' operations. "+ + builders.WithNumber("parallelism", + builders.Description("The parallelism factor of the source. Optional for 'create' and 'update' operations. "+ "Determines how many instances of the source will run concurrently. "+ "Higher values improve throughput but require more resources. "+ "Default is 1 (single instance). Recommended to align with both source capacity "+ "and destination topic partition count.")), - mcp.WithObject("source-config", - mcp.Description("User-defined source config key/values. Optional for 'create' and 'update' operations. "+ + builders.WithObject("source-config", + builders.Description("User-defined source config key/values. Optional for 'create' and 'update' operations. "+ "Provides configuration parameters specific to the source connector being used. "+ "For example, database connection details, Kafka bootstrap servers, credentials, etc. "+ "Specify as a JSON object with configuration properties required by the specific source type. "+ @@ -174,12 +174,12 @@ func (b *PulsarAdminSourcesToolBuilder) buildSourcesTool() mcp.Tool { } // buildSourcesHandler builds the Pulsar admin sources handler function -func (b *PulsarAdminSourcesToolBuilder) buildSourcesHandler(readOnly bool) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSourcesToolBuilder) buildSourcesHandler(readOnly bool) func(context.Context, *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Extract and validate operation parameter - operation, err := request.RequireString("operation") + operation, err := adapter.RequireString(request, "operation") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'operation': %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'operation': %v", err), nil } // Check if the operation is valid @@ -189,7 +189,7 @@ func (b *PulsarAdminSourcesToolBuilder) buildSourcesHandler(readOnly bool) func( } if !validOperations[operation] { - return mcp.NewToolResultError(fmt.Sprintf("Invalid operation: '%s'. Supported operations: list, get, status, create, update, delete, start, stop, restart, list-built-in", operation)), nil + return adapter.NewErrorResult("Invalid operation: '%s'. Supported operations: list, get, status, create, update, delete, start, stop, restart, list-built-in", operation), nil } // Check write permissions for write operations @@ -199,18 +199,18 @@ func (b *PulsarAdminSourcesToolBuilder) buildSourcesHandler(readOnly bool) func( } if readOnly && writeOperations[operation] { - return mcp.NewToolResultError(fmt.Sprintf("Operation '%s' not allowed in read-only mode. Read-only mode restricts modifications to Pulsar Sources.", operation)), nil + return adapter.NewErrorResult("Operation '%s' not allowed in read-only mode. Read-only mode restricts modifications to Pulsar Sources.", operation), nil } // Get Pulsar session from context session := mcpCtx.GetPulsarSession(ctx) if session == nil { - return mcp.NewToolResultError("Pulsar session not found in context"), nil + return adapter.NewErrorResult("Pulsar session not found in context"), nil } admin, err := session.GetAdminV3Client() if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get Pulsar client: %v", err)), nil + return adapter.NewErrorResult("Failed to get Pulsar client: %v", err), nil } // List built-in sources doesn't require tenant, namespace or name @@ -219,22 +219,22 @@ func (b *PulsarAdminSourcesToolBuilder) buildSourcesHandler(readOnly bool) func( } // Extract common parameters (all operations except list-built-in require tenant and namespace) - tenant, err := request.RequireString("tenant") + tenant, err := adapter.RequireString(request, "tenant") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'tenant': %v. A tenant is required for operation '%s'.", err, operation)), nil + return adapter.NewErrorResult("Missing required parameter 'tenant': %v. A tenant is required for operation '%s'.", err, operation), nil } - namespace, err := request.RequireString("namespace") + namespace, err := adapter.RequireString(request, "namespace") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'namespace': %v. A namespace is required for operation '%s'.", err, operation)), nil + return adapter.NewErrorResult("Missing required parameter 'namespace': %v. A namespace is required for operation '%s'.", err, operation), nil } // For all operations except 'list', name is required var name string if operation != "list" { - name, err = request.RequireString("name") + name, err = adapter.RequireString(request, "name") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'name' for operation '%s': %v. The source name must be specified for this operation.", operation, err)), nil + return adapter.NewErrorResult("Missing required parameter 'name' for operation '%s': %v. The source name must be specified for this operation.", operation, err), nil } } @@ -260,7 +260,7 @@ func (b *PulsarAdminSourcesToolBuilder) buildSourcesHandler(readOnly bool) func( return b.handleSourceRestart(ctx, admin, tenant, namespace, name) default: // This should never happen due to the valid operations check above - return mcp.NewToolResultError(fmt.Sprintf("Unsupported operation: %s", operation)), nil + return adapter.NewErrorResult("Unsupported operation: %s", operation), nil } } } @@ -268,71 +268,71 @@ func (b *PulsarAdminSourcesToolBuilder) buildSourcesHandler(readOnly bool) func( // Helper functions // handleSourceList handles listing all sources under a namespace -func (b *PulsarAdminSourcesToolBuilder) handleSourceList(_ context.Context, admin cmdutils.Client, tenant, namespace string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSourcesToolBuilder) handleSourceList(_ context.Context, admin cmdutils.Client, tenant, namespace string) (*mcpsdk.CallToolResult, error) { sources, err := admin.Sources().ListSources(tenant, namespace) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list sources in tenant '%s' namespace '%s': %v. Check that the tenant and namespace exist and you have proper permissions.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to list sources in tenant '%s' namespace '%s': %v. Check that the tenant and namespace exist and you have proper permissions.", tenant, namespace, err)), nil } // Convert result to JSON string sourcesJSON, err := json.Marshal(sources) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to serialize source list: %v", err)), nil + return adapter.NewErrorResult("Failed to serialize source list: %v", err), nil } - return mcp.NewToolResultText(string(sourcesJSON)), nil + return adapter.NewTextResult(string(sourcesJSON)), nil } // handleSourceGet handles getting information about a source -func (b *PulsarAdminSourcesToolBuilder) handleSourceGet(_ context.Context, admin cmdutils.Client, tenant, namespace, name string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSourcesToolBuilder) handleSourceGet(_ context.Context, admin cmdutils.Client, tenant, namespace, name string) (*mcpsdk.CallToolResult, error) { source, err := admin.Sources().GetSource(tenant, namespace, name) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get source '%s' in tenant '%s' namespace '%s': %v. Verify the source exists and you have proper permissions.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to get source '%s' in tenant '%s' namespace '%s': %v. Verify the source exists and you have proper permissions.", name, tenant, namespace, err)), nil } // Convert result to JSON string sourceJSON, err := json.Marshal(source) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to serialize source info: %v", err)), nil + return adapter.NewErrorResult("Failed to serialize source info: %v", err), nil } - return mcp.NewToolResultText(string(sourceJSON)), nil + return adapter.NewTextResult(string(sourceJSON)), nil } // handleSourceStatus handles getting the status of a source -func (b *PulsarAdminSourcesToolBuilder) handleSourceStatus(_ context.Context, admin cmdutils.Client, tenant, namespace, name string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSourcesToolBuilder) handleSourceStatus(_ context.Context, admin cmdutils.Client, tenant, namespace, name string) (*mcpsdk.CallToolResult, error) { status, err := admin.Sources().GetSourceStatus(tenant, namespace, name) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get status for source '%s' in tenant '%s' namespace '%s': %v. Verify the source exists and is properly deployed.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to get status for source '%s' in tenant '%s' namespace '%s': %v. Verify the source exists and is properly deployed.", name, tenant, namespace, err)), nil } // Convert result to JSON string statusJSON, err := json.Marshal(status) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to serialize source status: %v", err)), nil + return adapter.NewErrorResult("Failed to serialize source status: %v", err), nil } - return mcp.NewToolResultText(string(statusJSON)), nil + return adapter.NewTextResult(string(statusJSON)), nil } // handleSourceCreate handles creating a new source -func (b *PulsarAdminSourcesToolBuilder) handleSourceCreate(_ context.Context, admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - tenant, err := request.RequireString("tenant") +func (b *PulsarAdminSourcesToolBuilder) handleSourceCreate(_ context.Context, admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + tenant, err := adapter.RequireString(request, "tenant") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get tenant: %v", err)), nil + return adapter.NewErrorResult("Failed to get tenant: %v", err), nil } - namespace, err := request.RequireString("namespace") + namespace, err := adapter.RequireString(request, "namespace") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get namespace: %v", err)), nil + return adapter.NewErrorResult("Failed to get namespace: %v", err), nil } - name, err := request.RequireString("name") + name, err := adapter.RequireString(request, "name") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get name: %v", err)), nil + return adapter.NewErrorResult("Failed to get name: %v", err), nil } // Create a new SourceData object @@ -344,78 +344,74 @@ func (b *PulsarAdminSourcesToolBuilder) handleSourceCreate(_ context.Context, ad } // Get optional parameters - archive := request.GetString("archive", "") + archive := adapter.GetString(request, "archive", "") if archive != "" { sourceData.Archive = archive } - sourceType := request.GetString("source-type", "") + sourceType := adapter.GetString(request, "source-type", "") if sourceType != "" { sourceData.SourceType = sourceType } - destTopic := request.GetString("destination-topic-name", "") + destTopic := adapter.GetString(request, "destination-topic-name", "") if destTopic != "" { sourceData.DestinationTopicName = destTopic } - deserializationClassName := request.GetString("deserialization-classname", "") + deserializationClassName := adapter.GetString(request, "deserialization-classname", "") if deserializationClassName != "" { sourceData.DeserializationClassName = deserializationClassName } - schemaType := request.GetString("schema-type", "") + schemaType := adapter.GetString(request, "schema-type", "") if schemaType != "" { sourceData.SchemaType = schemaType } - className := request.GetString("classname", "") + className := adapter.GetString(request, "classname", "") if className != "" { sourceData.ClassName = className } - processingGuarantees := request.GetString("processing-guarantees", "") + processingGuarantees := adapter.GetString(request, "processing-guarantees", "") if processingGuarantees != "" { sourceData.ProcessingGuarantees = processingGuarantees } - parallelismFloat := request.GetFloat("parallelism", 1) + parallelismFloat := adapter.GetFloat(request, "parallelism", 1) if parallelismFloat >= 0 { sourceData.Parallelism = int(parallelismFloat) } // Get source config if available - var sourceConfigMap map[string]interface{} - sourceConfigObj, ok := request.GetArguments()["source-config"] - if ok && sourceConfigObj != nil { - if configMap, isMap := sourceConfigObj.(map[string]interface{}); isMap { - sourceConfigMap = configMap - // Convert to JSON string - sourceConfigJSON, err := json.Marshal(sourceConfigMap) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal source-config: %v. Ensure the source configuration is a valid JSON object.", err)), nil - } - sourceData.SourceConfigString = string(sourceConfigJSON) + sourceConfigMap := adapter.GetObject(request, "source-config") + if sourceConfigMap != nil { + // Convert to JSON string + sourceConfigJSON, err := json.Marshal(sourceConfigMap) + if err != nil { + return adapter.NewErrorResult("Failed to marshal source-config: %v. Ensure the source configuration is a valid JSON object.", err), nil } + sourceData.SourceConfigString = string(sourceConfigJSON) } // Validate inputs if sourceData.Archive == "" && sourceData.SourceType == "" { - return mcp.NewToolResultError("Missing required parameter: Either 'archive' or 'source-type' must be specified for source creation. Use 'archive' for custom connectors or 'source-type' for built-in connectors."), nil + return adapter.NewErrorResult("Missing required parameter: Either 'archive' or 'source-type' must be specified for source creation. Use 'archive' for custom connectors or 'source-type' for built-in connectors."), nil } if sourceData.Archive != "" && sourceData.SourceType != "" { - return mcp.NewToolResultError("Invalid parameters: Cannot specify both 'archive' and 'source-type'. Use only one of these parameters based on your connector type."), nil + return adapter.NewErrorResult("Invalid parameters: Cannot specify both 'archive' and 'source-type'. Use only one of these parameters based on your connector type."), nil } if sourceData.DestinationTopicName == "" { - return mcp.NewToolResultError("Missing required parameter: 'destination-topic-name' must be specified. This is the Pulsar topic where the source will publish data."), nil + return adapter.NewErrorResult("Missing required parameter: 'destination-topic-name' must be specified. This is the Pulsar topic where the source will publish data."), nil } // Process the arguments err = b.processSourceArguments(sourceData) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to process arguments: %v", err)), nil + return adapter.NewErrorResult("Failed to process arguments: %v", err), nil } // Create the source @@ -426,29 +422,29 @@ func (b *PulsarAdminSourcesToolBuilder) handleSourceCreate(_ context.Context, ad } if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to create source '%s' in tenant '%s' namespace '%s': %v. Verify all parameters are correct and required resources exist.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to create source '%s' in tenant '%s' namespace '%s': %v. Verify all parameters are correct and required resources exist.", name, tenant, namespace, err)), nil } - return mcp.NewToolResultText(fmt.Sprintf("Created source '%s' successfully in tenant '%s' namespace '%s'. The source will start pulling data from the external system and publishing to the destination topic.", + return adapter.NewTextResult(fmt.Sprintf("Created source '%s' successfully in tenant '%s' namespace '%s'. The source will start pulling data from the external system and publishing to the destination topic.", name, tenant, namespace)), nil } // handleSourceUpdate handles updating an existing source -func (b *PulsarAdminSourcesToolBuilder) handleSourceUpdate(_ context.Context, admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - tenant, err := request.RequireString("tenant") +func (b *PulsarAdminSourcesToolBuilder) handleSourceUpdate(_ context.Context, admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + tenant, err := adapter.RequireString(request, "tenant") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get tenant: %v", err)), nil + return adapter.NewErrorResult("Failed to get tenant: %v", err), nil } - namespace, err := request.RequireString("namespace") + namespace, err := adapter.RequireString(request, "namespace") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get namespace: %v", err)), nil + return adapter.NewErrorResult("Failed to get namespace: %v", err), nil } - name, err := request.RequireString("name") + name, err := adapter.RequireString(request, "name") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get name: %v", err)), nil + return adapter.NewErrorResult("Failed to get name: %v", err), nil } // Create a new SourceData object @@ -460,70 +456,66 @@ func (b *PulsarAdminSourcesToolBuilder) handleSourceUpdate(_ context.Context, ad } // Get optional parameters - archive := request.GetString("archive", "") + archive := adapter.GetString(request, "archive", "") if archive != "" { sourceData.Archive = archive } - sourceType := request.GetString("source-type", "") + sourceType := adapter.GetString(request, "source-type", "") if sourceType != "" { sourceData.SourceType = sourceType } - destTopic := request.GetString("destination-topic-name", "") + destTopic := adapter.GetString(request, "destination-topic-name", "") if destTopic != "" { sourceData.DestinationTopicName = destTopic } - deserializationClassName := request.GetString("deserialization-classname", "") + deserializationClassName := adapter.GetString(request, "deserialization-classname", "") if deserializationClassName != "" { sourceData.DeserializationClassName = deserializationClassName } - schemaType := request.GetString("schema-type", "") + schemaType := adapter.GetString(request, "schema-type", "") if schemaType != "" { sourceData.SchemaType = schemaType } - className := request.GetString("classname", "") + className := adapter.GetString(request, "classname", "") if className != "" { sourceData.ClassName = className } - processingGuarantees := request.GetString("processing-guarantees", "") + processingGuarantees := adapter.GetString(request, "processing-guarantees", "") if processingGuarantees != "" { sourceData.ProcessingGuarantees = processingGuarantees } - parallelismFloat := request.GetFloat("parallelism", 1) + parallelismFloat := adapter.GetFloat(request, "parallelism", 1) if parallelismFloat >= 0 { sourceData.Parallelism = int(parallelismFloat) } // Get source config if available - var sourceConfigMap map[string]interface{} - sourceConfigObj, ok := request.GetArguments()["source-config"] - if ok && sourceConfigObj != nil { - if configMap, isMap := sourceConfigObj.(map[string]interface{}); isMap { - sourceConfigMap = configMap - // Convert to JSON string - sourceConfigJSON, err := json.Marshal(sourceConfigMap) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal source-config: %v. Ensure the source configuration is a valid JSON object.", err)), nil - } - sourceData.SourceConfigString = string(sourceConfigJSON) + sourceConfigMap := adapter.GetObject(request, "source-config") + if sourceConfigMap != nil { + // Convert to JSON string + sourceConfigJSON, err := json.Marshal(sourceConfigMap) + if err != nil { + return adapter.NewErrorResult("Failed to marshal source-config: %v. Ensure the source configuration is a valid JSON object.", err), nil } + sourceData.SourceConfigString = string(sourceConfigJSON) } // Validate inputs if both are specified if sourceData.Archive != "" && sourceData.SourceType != "" { - return mcp.NewToolResultError("Invalid parameters: Cannot specify both 'archive' and 'source-type'. Use only one of these parameters based on your connector type."), nil + return adapter.NewErrorResult("Invalid parameters: Cannot specify both 'archive' and 'source-type'. Use only one of these parameters based on your connector type."), nil } // Process the arguments err = b.processSourceArguments(sourceData) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to process arguments: %v", err)), nil + return adapter.NewErrorResult("Failed to process arguments: %v", err), nil } // Create update options @@ -539,76 +531,76 @@ func (b *PulsarAdminSourcesToolBuilder) handleSourceUpdate(_ context.Context, ad } if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to update source '%s' in tenant '%s' namespace '%s': %v. Verify the source exists and all parameters are valid.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to update source '%s' in tenant '%s' namespace '%s': %v. Verify the source exists and all parameters are valid.", name, tenant, namespace, err)), nil } - return mcp.NewToolResultText(fmt.Sprintf("Updated source '%s' successfully in tenant '%s' namespace '%s'. The source may need to be restarted to apply all changes.", + return adapter.NewTextResult(fmt.Sprintf("Updated source '%s' successfully in tenant '%s' namespace '%s'. The source may need to be restarted to apply all changes.", name, tenant, namespace)), nil } // handleSourceDelete handles deleting a source -func (b *PulsarAdminSourcesToolBuilder) handleSourceDelete(_ context.Context, admin cmdutils.Client, tenant, namespace, name string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSourcesToolBuilder) handleSourceDelete(_ context.Context, admin cmdutils.Client, tenant, namespace, name string) (*mcpsdk.CallToolResult, error) { err := admin.Sources().DeleteSource(tenant, namespace, name) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to delete source '%s' in tenant '%s' namespace '%s': %v. Verify the source exists and you have deletion permissions.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to delete source '%s' in tenant '%s' namespace '%s': %v. Verify the source exists and you have deletion permissions.", name, tenant, namespace, err)), nil } - return mcp.NewToolResultText(fmt.Sprintf("Deleted source '%s' successfully from tenant '%s' namespace '%s'. All running instances have been terminated.", + return adapter.NewTextResult(fmt.Sprintf("Deleted source '%s' successfully from tenant '%s' namespace '%s'. All running instances have been terminated.", name, tenant, namespace)), nil } // handleSourceStart handles starting a source -func (b *PulsarAdminSourcesToolBuilder) handleSourceStart(_ context.Context, admin cmdutils.Client, tenant, namespace, name string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSourcesToolBuilder) handleSourceStart(_ context.Context, admin cmdutils.Client, tenant, namespace, name string) (*mcpsdk.CallToolResult, error) { err := admin.Sources().StartSource(tenant, namespace, name) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to start source '%s' in tenant '%s' namespace '%s': %v. Verify the source exists and is not already running.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to start source '%s' in tenant '%s' namespace '%s': %v. Verify the source exists and is not already running.", name, tenant, namespace, err)), nil } - return mcp.NewToolResultText(fmt.Sprintf("Started source '%s' successfully in tenant '%s' namespace '%s'. The source will begin pulling data from the external system.", + return adapter.NewTextResult(fmt.Sprintf("Started source '%s' successfully in tenant '%s' namespace '%s'. The source will begin pulling data from the external system.", name, tenant, namespace)), nil } // handleSourceStop handles stopping a source -func (b *PulsarAdminSourcesToolBuilder) handleSourceStop(_ context.Context, admin cmdutils.Client, tenant, namespace, name string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSourcesToolBuilder) handleSourceStop(_ context.Context, admin cmdutils.Client, tenant, namespace, name string) (*mcpsdk.CallToolResult, error) { err := admin.Sources().StopSource(tenant, namespace, name) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to stop source '%s' in tenant '%s' namespace '%s': %v. Verify the source exists and is currently running.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to stop source '%s' in tenant '%s' namespace '%s': %v. Verify the source exists and is currently running.", name, tenant, namespace, err)), nil } - return mcp.NewToolResultText(fmt.Sprintf("Stopped source '%s' successfully in tenant '%s' namespace '%s'. The source will no longer pull data until restarted.", + return adapter.NewTextResult(fmt.Sprintf("Stopped source '%s' successfully in tenant '%s' namespace '%s'. The source will no longer pull data until restarted.", name, tenant, namespace)), nil } // handleSourceRestart handles restarting a source -func (b *PulsarAdminSourcesToolBuilder) handleSourceRestart(_ context.Context, admin cmdutils.Client, tenant, namespace, name string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSourcesToolBuilder) handleSourceRestart(_ context.Context, admin cmdutils.Client, tenant, namespace, name string) (*mcpsdk.CallToolResult, error) { err := admin.Sources().RestartSource(tenant, namespace, name) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to restart source '%s' in tenant '%s' namespace '%s': %v. Verify the source exists and is properly deployed.", + return adapter.NewErrorResult(fmt.Sprintf("Failed to restart source '%s' in tenant '%s' namespace '%s': %v. Verify the source exists and is properly deployed.", name, tenant, namespace, err)), nil } - return mcp.NewToolResultText(fmt.Sprintf("Restarted source '%s' successfully in tenant '%s' namespace '%s'. All source instances have been restarted.", + return adapter.NewTextResult(fmt.Sprintf("Restarted source '%s' successfully in tenant '%s' namespace '%s'. All source instances have been restarted.", name, tenant, namespace)), nil } // handleListBuiltInSources handles listing all built-in source connectors -func (b *PulsarAdminSourcesToolBuilder) handleListBuiltInSources(_ context.Context, admin cmdutils.Client) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSourcesToolBuilder) handleListBuiltInSources(_ context.Context, admin cmdutils.Client) (*mcpsdk.CallToolResult, error) { sources, err := admin.Sources().GetBuiltInSources() if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list built-in sources: %v. There might be an issue connecting to the Pulsar cluster.", err)), nil + return adapter.NewErrorResult("Failed to list built-in sources: %v. There might be an issue connecting to the Pulsar cluster.", err), nil } // Convert result to JSON string sourcesJSON, err := json.Marshal(sources) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to serialize built-in sources: %v", err)), nil + return adapter.NewErrorResult("Failed to serialize built-in sources: %v", err), nil } - return mcp.NewToolResultText(string(sourcesJSON)), nil + return adapter.NewTextResult(string(sourcesJSON)), nil } // processSourceArguments is a simplified version of the pulsarctl function to process source arguments diff --git a/pkg/mcp/builders/pulsar/subscription.go b/pkg/mcp/builders/pulsar/subscription.go index f1a1882..0daad0b 100644 --- a/pkg/mcp/builders/pulsar/subscription.go +++ b/pkg/mcp/builders/pulsar/subscription.go @@ -21,10 +21,10 @@ import ( "strings" "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/pulsarctl/pkg/cmdutils" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" mcpCtx "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" ) @@ -59,7 +59,7 @@ func NewPulsarAdminSubscriptionToolBuilder() *PulsarAdminSubscriptionToolBuilder // BuildTools builds the Pulsar Admin Subscription tool list // This is the core method implementing the ToolBuilder interface -func (b *PulsarAdminSubscriptionToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]server.ServerTool, error) { +func (b *PulsarAdminSubscriptionToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]builders.ServerTool, error) { // Check features - return empty list if no required features are present if !b.HasAnyRequiredFeature(config.Features) { return nil, nil @@ -74,7 +74,7 @@ func (b *PulsarAdminSubscriptionToolBuilder) BuildTools(_ context.Context, confi tool := b.buildSubscriptionTool() handler := b.buildSubscriptionHandler(config.ReadOnly) - return []server.ServerTool{ + return []builders.ServerTool{ { Tool: tool, Handler: handler, @@ -84,7 +84,7 @@ func (b *PulsarAdminSubscriptionToolBuilder) BuildTools(_ context.Context, confi // buildSubscriptionTool builds the Pulsar Admin Subscription MCP tool definition // Migrated from the original tool definition logic -func (b *PulsarAdminSubscriptionToolBuilder) buildSubscriptionTool() mcp.Tool { +func (b *PulsarAdminSubscriptionToolBuilder) buildSubscriptionTool() *mcpsdk.Tool { toolDesc := "Manage Apache Pulsar subscriptions on topics. " + "Subscriptions are named entities representing consumer groups that maintain their position in a topic. " + "Pulsar supports multiple subscription modes (Exclusive, Shared, Failover, Key_Shared) to accommodate different messaging patterns. " + @@ -104,41 +104,41 @@ func (b *PulsarAdminSubscriptionToolBuilder) buildSubscriptionTool() mcp.Tool { "- expire: Expire messages older than specified time for a subscription\n" + "- reset-cursor: Reset the cursor position for a subscription to a specific message ID" - return mcp.NewTool("pulsar_admin_subscription", - mcp.WithDescription(toolDesc), - mcp.WithString("resource", mcp.Required(), - mcp.Description(resourceDesc), + return builders.NewTool("pulsar_admin_subscription", + builders.WithDescription(toolDesc), + builders.WithString("resource", builders.Required(), + builders.Description(resourceDesc), ), - mcp.WithString("operation", mcp.Required(), - mcp.Description(operationDesc), + builders.WithString("operation", builders.Required(), + builders.Description(operationDesc), ), - mcp.WithString("topic", mcp.Required(), - mcp.Description("The fully qualified topic name in the format 'persistent://tenant/namespace/topic'. "+ + builders.WithString("topic", builders.Required(), + builders.Description("The fully qualified topic name in the format 'persistent://tenant/namespace/topic'. "+ "For partitioned topics, you can either specify the base topic name (to apply the operation across all partitions) "+ "or a specific partition in the format 'topicName-partition-N'."), ), - mcp.WithString("subscription", - mcp.Description("The subscription name. Required for all operations except 'list'. "+ + builders.WithString("subscription", + builders.Description("The subscription name. Required for all operations except 'list'. "+ "A subscription name is a logical identifier for a durable position in a topic. "+ "Multiple consumers can attach to the same subscription to implement different messaging patterns."), ), - mcp.WithString("messageId", - mcp.Description("Message ID for positioning the subscription cursor. Used in 'create' and 'reset-cursor' operations. "+ + builders.WithString("messageId", + builders.Description("Message ID for positioning the subscription cursor. Used in 'create' and 'reset-cursor' operations. "+ "Values can be:\n"+ "- 'latest': Position at the latest (most recent) message\n"+ "- 'earliest': Position at the earliest (oldest available) message\n"+ "- specific position in 'ledgerId:entryId' format for precise positioning"), ), - mcp.WithNumber("count", - mcp.Description("The number of messages to skip (required for 'skip' operation). "+ + builders.WithNumber("count", + builders.Description("The number of messages to skip (required for 'skip' operation). "+ "This moves the subscription cursor forward by the specified number of messages without processing them."), ), - mcp.WithNumber("expireTimeInSeconds", - mcp.Description("Expire messages older than the specified seconds (required for 'expire' operation). "+ + builders.WithNumber("expireTimeInSeconds", + builders.Description("Expire messages older than the specified seconds (required for 'expire' operation). "+ "This moves the subscription cursor to skip all messages published before the specified time."), ), - mcp.WithBoolean("force", - mcp.Description("Force deletion of subscription (optional for 'delete' operation). "+ + builders.WithBoolean("force", + builders.Description("Force deletion of subscription (optional for 'delete' operation). "+ "When true, all consumers will be forcefully disconnected and the subscription will be deleted. "+ "Use with caution as it can interrupt active message processing."), ), @@ -147,22 +147,22 @@ func (b *PulsarAdminSubscriptionToolBuilder) buildSubscriptionTool() mcp.Tool { // buildSubscriptionHandler builds the Pulsar Admin Subscription handler function // Migrated from the original handler logic -func (b *PulsarAdminSubscriptionToolBuilder) buildSubscriptionHandler(readOnly bool) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSubscriptionToolBuilder) buildSubscriptionHandler(readOnly bool) func(context.Context, *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - resource, err := request.RequireString("resource") + resource, err := adapter.RequireString(request, "resource") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get resource: %v", err)), nil + return adapter.NewErrorResult("Failed to get resource: %v", err), nil } - operation, err := request.RequireString("operation") + operation, err := adapter.RequireString(request, "operation") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get operation: %v", err)), nil + return adapter.NewErrorResult("Failed to get operation: %v", err), nil } - topic, err := request.RequireString("topic") + topic, err := adapter.RequireString(request, "topic") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'topic'. Please provide the fully qualified topic name: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'topic'. Please provide the fully qualified topic name: %v", err), nil } // Normalize parameters @@ -171,30 +171,30 @@ func (b *PulsarAdminSubscriptionToolBuilder) buildSubscriptionHandler(readOnly b // Validate write operations in read-only mode if readOnly && (operation != "list") { - return mcp.NewToolResultError("Write operations are not allowed in read-only mode"), nil + return adapter.NewErrorResult("Write operations are not allowed in read-only mode"), nil } // Verify resource type if resource != "subscription" { - return mcp.NewToolResultError(fmt.Sprintf("Invalid resource: %s. Only 'subscription' is supported", resource)), nil + return adapter.NewErrorResult("Invalid resource: %s. Only 'subscription' is supported", resource), nil } // Parse topic name topicName, err := utils.GetTopicName(topic) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid topic name '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Invalid topic name '%s': %v", topic, err), nil } // Get Pulsar session from context session := mcpCtx.GetPulsarSession(ctx) if session == nil { - return mcp.NewToolResultError("Pulsar session not found in context"), nil + return adapter.NewErrorResult("Pulsar session not found in context"), nil } // Create the admin client admin, err := session.GetAdminClient() if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get admin client: %v", err)), nil + return adapter.NewErrorResult("Failed to get admin client: %v", err), nil } // Dispatch based on operation @@ -212,7 +212,7 @@ func (b *PulsarAdminSubscriptionToolBuilder) buildSubscriptionHandler(readOnly b case "reset-cursor": return b.handleSubsResetCursor(admin, topicName, request) default: - return mcp.NewToolResultError(fmt.Sprintf("Unknown operation: %s", operation)), nil + return adapter.NewErrorResult("Unknown operation: %s", operation), nil } } } @@ -220,27 +220,27 @@ func (b *PulsarAdminSubscriptionToolBuilder) buildSubscriptionHandler(readOnly b // Unified error handling and utility functions // handleError provides unified error handling -func (b *PulsarAdminSubscriptionToolBuilder) handleError(operation string, err error) *mcp.CallToolResult { - return mcp.NewToolResultError(fmt.Sprintf("Failed to %s: %v", operation, err)) +func (b *PulsarAdminSubscriptionToolBuilder) handleError(operation string, err error) *mcpsdk.CallToolResult { + return adapter.NewErrorResult("Failed to %s: %v", operation, err) } // marshalResponse provides unified JSON serialization for responses -func (b *PulsarAdminSubscriptionToolBuilder) marshalResponse(data interface{}) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSubscriptionToolBuilder) marshalResponse(data interface{}) (*mcpsdk.CallToolResult, error) { jsonBytes, err := json.Marshal(data) if err != nil { return b.handleError("marshal response", err), nil } - return mcp.NewToolResultText(string(jsonBytes)), nil + return adapter.NewTextResult(string(jsonBytes)), nil } // Operation handler functions - migrated from the original implementation // handleSubsList handles listing all subscriptions for a topic -func (b *PulsarAdminSubscriptionToolBuilder) handleSubsList(admin cmdutils.Client, topicName *utils.TopicName) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSubscriptionToolBuilder) handleSubsList(admin cmdutils.Client, topicName *utils.TopicName) (*mcpsdk.CallToolResult, error) { // List subscriptions subscriptions, err := admin.Subscriptions().List(*topicName) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list subscriptions for topic '%s': %v", + return adapter.NewErrorResult(fmt.Sprintf("Failed to list subscriptions for topic '%s': %v", topicName.String(), err)), nil } @@ -248,15 +248,15 @@ func (b *PulsarAdminSubscriptionToolBuilder) handleSubsList(admin cmdutils.Clien } // handleSubsCreate handles creating a new subscription -func (b *PulsarAdminSubscriptionToolBuilder) handleSubsCreate(admin cmdutils.Client, topicName *utils.TopicName, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSubscriptionToolBuilder) handleSubsCreate(admin cmdutils.Client, topicName *utils.TopicName, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameter - subscription, err := request.RequireString("subscription") + subscription, err := adapter.RequireString(request, "subscription") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'subscription' for subscription.create: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'subscription' for subscription.create: %v", err), nil } // Get optional messageID parameter (default is "latest") - messageID := request.GetString("messageId", "latest") + messageID := adapter.GetString(request, "messageId", "latest") // Parse messageId var messageIDObj utils.MessageID @@ -268,12 +268,12 @@ func (b *PulsarAdminSubscriptionToolBuilder) handleSubsCreate(admin cmdutils.Cli default: s := strings.Split(messageID, ":") if len(s) != 2 { - return mcp.NewToolResultError(fmt.Sprintf( + return adapter.NewErrorResult(fmt.Sprintf( "Invalid messageId format: %s. Use 'latest', 'earliest', or 'ledgerId:entryId' format", messageID)), nil } msgID, err := utils.ParseMessageID(messageID) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to parse messageId '%s': %v", messageID, err)), nil + return adapter.NewErrorResult("Failed to parse messageId '%s': %v", messageID, err), nil } messageIDObj = *msgID } @@ -281,36 +281,36 @@ func (b *PulsarAdminSubscriptionToolBuilder) handleSubsCreate(admin cmdutils.Cli // Create subscription err = admin.Subscriptions().Create(*topicName, subscription, messageIDObj) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to create subscription '%s' on topic '%s': %v", + return adapter.NewErrorResult(fmt.Sprintf("Failed to create subscription '%s' on topic '%s': %v", subscription, topicName.String(), err)), nil } - return mcp.NewToolResultText(fmt.Sprintf("Created subscription '%s' on topic '%s' from position '%s' successfully", + return adapter.NewTextResult(fmt.Sprintf("Created subscription '%s' on topic '%s' from position '%s' successfully", subscription, topicName.String(), messageID)), nil } // handleSubsDelete handles deleting a subscription -func (b *PulsarAdminSubscriptionToolBuilder) handleSubsDelete(admin cmdutils.Client, topicName *utils.TopicName, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSubscriptionToolBuilder) handleSubsDelete(admin cmdutils.Client, topicName *utils.TopicName, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameter - subscription, err := request.RequireString("subscription") + subscription, err := adapter.RequireString(request, "subscription") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'subscription' for subscription.delete: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'subscription' for subscription.delete: %v", err), nil } // Get optional force parameter (default is false) - force := request.GetBool("force", false) + force := adapter.GetBool(request, "force", false) // Delete subscription if force { err = admin.Subscriptions().ForceDelete(*topicName, subscription) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to forcefully delete subscription '%s' from topic '%s': %v", + return adapter.NewErrorResult(fmt.Sprintf("Failed to forcefully delete subscription '%s' from topic '%s': %v", subscription, topicName.String(), err)), nil } } else { err = admin.Subscriptions().Delete(*topicName, subscription) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to delete subscription '%s' from topic '%s': %v", + return adapter.NewErrorResult(fmt.Sprintf("Failed to delete subscription '%s' from topic '%s': %v", subscription, topicName.String(), err)), nil } } @@ -319,71 +319,71 @@ func (b *PulsarAdminSubscriptionToolBuilder) handleSubsDelete(admin cmdutils.Cli if force { forceStr = " forcefully" } - return mcp.NewToolResultText(fmt.Sprintf("Deleted subscription '%s' from topic '%s'%s successfully", + return adapter.NewTextResult(fmt.Sprintf("Deleted subscription '%s' from topic '%s'%s successfully", subscription, topicName.String(), forceStr)), nil } // handleSubsSkip handles skipping messages for a subscription -func (b *PulsarAdminSubscriptionToolBuilder) handleSubsSkip(admin cmdutils.Client, topicName *utils.TopicName, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSubscriptionToolBuilder) handleSubsSkip(admin cmdutils.Client, topicName *utils.TopicName, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - subscription, err := request.RequireString("subscription") + subscription, err := adapter.RequireString(request, "subscription") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'subscription' for subscription.skip: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'subscription' for subscription.skip: %v", err), nil } - count, err := request.RequireFloat("count") + count, err := adapter.RequireFloat(request, "count") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'count' for subscription.skip: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'count' for subscription.skip: %v", err), nil } // Skip messages err = admin.Subscriptions().SkipMessages(*topicName, subscription, int64(count)) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to skip messages for subscription '%s' on topic '%s': %v", + return adapter.NewErrorResult(fmt.Sprintf("Failed to skip messages for subscription '%s' on topic '%s': %v", subscription, topicName.String(), err)), nil } - return mcp.NewToolResultText(fmt.Sprintf("Skipped %d messages for subscription '%s' on topic '%s' successfully", + return adapter.NewTextResult(fmt.Sprintf("Skipped %d messages for subscription '%s' on topic '%s' successfully", int(count), subscription, topicName.String())), nil } // handleSubsExpire handles expiring messages for a subscription -func (b *PulsarAdminSubscriptionToolBuilder) handleSubsExpire(admin cmdutils.Client, topicName *utils.TopicName, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSubscriptionToolBuilder) handleSubsExpire(admin cmdutils.Client, topicName *utils.TopicName, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - subscription, err := request.RequireString("subscription") + subscription, err := adapter.RequireString(request, "subscription") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'subscription' for subscription.expire: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'subscription' for subscription.expire: %v", err), nil } - expireTime, err := request.RequireFloat("expireTimeInSeconds") + expireTime, err := adapter.RequireFloat(request, "expireTimeInSeconds") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'expireTimeInSeconds' for subscription.expire: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'expireTimeInSeconds' for subscription.expire: %v", err), nil } // Expire messages err = admin.Subscriptions().ExpireMessages(*topicName, subscription, int64(expireTime)) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to expire messages for subscription '%s' on topic '%s': %v", + return adapter.NewErrorResult(fmt.Sprintf("Failed to expire messages for subscription '%s' on topic '%s': %v", subscription, topicName.String(), err)), nil } - return mcp.NewToolResultText( + return adapter.NewTextResult( fmt.Sprintf("Expired messages older than %d seconds for subscription '%s' on topic '%s' successfully", int(expireTime), subscription, topicName.String()), ), nil } // handleSubsResetCursor handles resetting a subscription cursor -func (b *PulsarAdminSubscriptionToolBuilder) handleSubsResetCursor(admin cmdutils.Client, topicName *utils.TopicName, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminSubscriptionToolBuilder) handleSubsResetCursor(admin cmdutils.Client, topicName *utils.TopicName, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - subscription, err := request.RequireString("subscription") + subscription, err := adapter.RequireString(request, "subscription") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'subscription' for subscription.reset-cursor: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'subscription' for subscription.reset-cursor: %v", err), nil } - messageID, err := request.RequireString("messageId") + messageID, err := adapter.RequireString(request, "messageId") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'messageId' for subscription.reset-cursor: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'messageId' for subscription.reset-cursor: %v", err), nil } // Parse messageId @@ -396,12 +396,12 @@ func (b *PulsarAdminSubscriptionToolBuilder) handleSubsResetCursor(admin cmdutil default: s := strings.Split(messageID, ":") if len(s) != 2 { - return mcp.NewToolResultError(fmt.Sprintf( + return adapter.NewErrorResult(fmt.Sprintf( "Invalid messageId format: %s. Use 'latest', 'earliest', or 'ledgerId:entryId' format", messageID)), nil } msgID, err := utils.ParseMessageID(messageID) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to parse messageId '%s': %v", messageID, err)), nil + return adapter.NewErrorResult("Failed to parse messageId '%s': %v", messageID, err), nil } messageIDObj = *msgID } @@ -409,11 +409,11 @@ func (b *PulsarAdminSubscriptionToolBuilder) handleSubsResetCursor(admin cmdutil // Reset cursor err = admin.Subscriptions().ResetCursorToMessageID(*topicName, subscription, messageIDObj) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to reset cursor for subscription '%s' on topic '%s': %v", + return adapter.NewErrorResult(fmt.Sprintf("Failed to reset cursor for subscription '%s' on topic '%s': %v", subscription, topicName.String(), err)), nil } - return mcp.NewToolResultText( + return adapter.NewTextResult( fmt.Sprintf("Reset cursor for subscription '%s' on topic '%s' to position '%s' successfully", subscription, topicName.String(), messageID), ), nil diff --git a/pkg/mcp/builders/pulsar/tenant.go b/pkg/mcp/builders/pulsar/tenant.go index 8a0d457..cd4dac9 100644 --- a/pkg/mcp/builders/pulsar/tenant.go +++ b/pkg/mcp/builders/pulsar/tenant.go @@ -21,10 +21,10 @@ import ( "strings" "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/pulsarctl/pkg/cmdutils" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" mcpCtx "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" ) @@ -59,7 +59,7 @@ func NewPulsarAdminTenantToolBuilder() *PulsarAdminTenantToolBuilder { // BuildTools builds the Pulsar Admin Tenant tool list // This is the core method implementing the ToolBuilder interface -func (b *PulsarAdminTenantToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]server.ServerTool, error) { +func (b *PulsarAdminTenantToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]builders.ServerTool, error) { // Check features - return empty list if no required features are present if !b.HasAnyRequiredFeature(config.Features) { return nil, nil @@ -74,7 +74,7 @@ func (b *PulsarAdminTenantToolBuilder) BuildTools(_ context.Context, config buil tool := b.buildTenantTool() handler := b.buildTenantHandler(config.ReadOnly) - return []server.ServerTool{ + return []builders.ServerTool{ { Tool: tool, Handler: handler, @@ -84,7 +84,7 @@ func (b *PulsarAdminTenantToolBuilder) BuildTools(_ context.Context, config buil // buildTenantTool builds the Pulsar Admin Tenant MCP tool definition // Migrated from the original tool definition logic -func (b *PulsarAdminTenantToolBuilder) buildTenantTool() mcp.Tool { +func (b *PulsarAdminTenantToolBuilder) buildTenantTool() *mcpsdk.Tool { toolDesc := "Manage Apache Pulsar tenants. " + "Tenants are the highest level administrative unit in Pulsar's multi-tenancy hierarchy. " + "Each tenant can contain multiple namespaces, allowing for logical isolation of applications. " + @@ -104,39 +104,39 @@ func (b *PulsarAdminTenantToolBuilder) buildTenantTool() mcp.Tool { "- update: Update configuration for an existing tenant\n" + "- delete: Delete an existing tenant (must not have any active namespaces)" - return mcp.NewTool("pulsar_admin_tenant", - mcp.WithDescription(toolDesc), - mcp.WithString("resource", mcp.Required(), - mcp.Description(resourceDesc), + return builders.NewTool("pulsar_admin_tenant", + builders.WithDescription(toolDesc), + builders.WithString("resource", builders.Required(), + builders.Description(resourceDesc), ), - mcp.WithString("operation", mcp.Required(), - mcp.Description(operationDesc), + builders.WithString("operation", builders.Required(), + builders.Description(operationDesc), ), - mcp.WithString("tenant", - mcp.Description("The tenant name to operate on. Required for all operations except 'list'. "+ + builders.WithString("tenant", + builders.Description("The tenant name to operate on. Required for all operations except 'list'. "+ "Tenant names are unique identifiers and form the root of the topic naming hierarchy. "+ "A valid tenant name must be comprised of alphanumeric characters and/or the following special characters: "+ "'-', '_', '.', ':'. Ensure the tenant name follows your organization's naming conventions."), ), - mcp.WithArray("adminRoles", - mcp.Description("List of auth principals (users or roles) allowed to administrate the tenant. "+ + builders.WithArray("adminRoles", + builders.Description("List of auth principals (users or roles) allowed to administrate the tenant. "+ "Required for 'create' and 'update' operations. These roles can create, update, or delete any "+ "namespaces within the tenant, and can manage topic configurations. "+ "Format: array of role strings, e.g., ['admin1', 'orgAdmin']. "+ "Use empty array [] to remove all admin roles."), - mcp.Items( + builders.Items( map[string]interface{}{ "type": "string", "description": "role", }, ), ), - mcp.WithArray("allowedClusters", - mcp.Description("List of clusters that this tenant can access. Required for 'create' and 'update' operations. "+ + builders.WithArray("allowedClusters", + builders.Description("List of clusters that this tenant can access. Required for 'create' and 'update' operations. "+ "Restricts the tenant to only use specified clusters, enabling geographic or infrastructure isolation. "+ "Format: array of cluster names, e.g., ['us-west', 'us-east']. "+ "An empty list means no clusters are accessible to this tenant."), - mcp.Items( + builders.Items( map[string]interface{}{ "type": "string", "description": "cluster", @@ -148,17 +148,17 @@ func (b *PulsarAdminTenantToolBuilder) buildTenantTool() mcp.Tool { // buildTenantHandler builds the Pulsar Admin Tenant handler function // Migrated from the original handler logic -func (b *PulsarAdminTenantToolBuilder) buildTenantHandler(readOnly bool) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTenantToolBuilder) buildTenantHandler(readOnly bool) func(context.Context, *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - resource, err := request.RequireString("resource") + resource, err := adapter.RequireString(request, "resource") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get resource: %v", err)), nil + return adapter.NewErrorResult("Failed to get resource: %v", err), nil } - operation, err := request.RequireString("operation") + operation, err := adapter.RequireString(request, "operation") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get operation: %v", err)), nil + return adapter.NewErrorResult("Failed to get operation: %v", err), nil } // Normalize parameters @@ -167,24 +167,24 @@ func (b *PulsarAdminTenantToolBuilder) buildTenantHandler(readOnly bool) func(co // Validate resource if resource != "tenant" { - return mcp.NewToolResultError(fmt.Sprintf("Invalid resource: %s. Only 'tenant' is supported.", resource)), nil + return adapter.NewErrorResult("Invalid resource: %s. Only 'tenant' is supported.", resource), nil } // Validate write operations in read-only mode if readOnly && (operation == "create" || operation == "update" || operation == "delete") { - return mcp.NewToolResultError("Write operations are not allowed in read-only mode"), nil + return adapter.NewErrorResult("Write operations are not allowed in read-only mode"), nil } // Get Pulsar session from context session := mcpCtx.GetPulsarSession(ctx) if session == nil { - return mcp.NewToolResultError("Pulsar session not found in context"), nil + return adapter.NewErrorResult("Pulsar session not found in context"), nil } // Create the admin client admin, err := session.GetAdminClient() if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get admin client: %v", err)), nil + return adapter.NewErrorResult("Failed to get admin client: %v", err), nil } // Dispatch based on operation @@ -200,7 +200,7 @@ func (b *PulsarAdminTenantToolBuilder) buildTenantHandler(readOnly bool) func(co case "delete": return b.handleTenantDelete(admin, request) default: - return mcp.NewToolResultError(fmt.Sprintf("Unknown operation: %s", operation)), nil + return adapter.NewErrorResult("Unknown operation: %s", operation), nil } } } @@ -208,23 +208,23 @@ func (b *PulsarAdminTenantToolBuilder) buildTenantHandler(readOnly bool) func(co // Unified error handling and utility functions // handleError provides unified error handling -func (b *PulsarAdminTenantToolBuilder) handleError(operation string, err error) *mcp.CallToolResult { - return mcp.NewToolResultError(fmt.Sprintf("Failed to %s: %v", operation, err)) +func (b *PulsarAdminTenantToolBuilder) handleError(operation string, err error) *mcpsdk.CallToolResult { + return adapter.NewErrorResult("Failed to %s: %v", operation, err) } // marshalResponse provides unified JSON serialization for responses -func (b *PulsarAdminTenantToolBuilder) marshalResponse(data interface{}) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTenantToolBuilder) marshalResponse(data interface{}) (*mcpsdk.CallToolResult, error) { jsonBytes, err := json.Marshal(data) if err != nil { return b.handleError("marshal response", err), nil } - return mcp.NewToolResultText(string(jsonBytes)), nil + return adapter.NewTextResult(string(jsonBytes)), nil } // Operation handler functions - migrated from the original implementation // handleTenantsList handles listing all tenants -func (b *PulsarAdminTenantToolBuilder) handleTenantsList(admin cmdutils.Client) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTenantToolBuilder) handleTenantsList(admin cmdutils.Client) (*mcpsdk.CallToolResult, error) { // Get tenants list tenants, err := admin.Tenants().List() if err != nil { @@ -235,10 +235,10 @@ func (b *PulsarAdminTenantToolBuilder) handleTenantsList(admin cmdutils.Client) } // handleTenantGet handles getting tenant configuration -func (b *PulsarAdminTenantToolBuilder) handleTenantGet(admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - tenant, err := request.RequireString("tenant") +func (b *PulsarAdminTenantToolBuilder) handleTenantGet(admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + tenant, err := adapter.RequireString(request, "tenant") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get tenant name: %v", err)), nil + return adapter.NewErrorResult("Failed to get tenant name: %v", err), nil } // Get tenant info @@ -251,20 +251,20 @@ func (b *PulsarAdminTenantToolBuilder) handleTenantGet(admin cmdutils.Client, re } // handleTenantCreate handles creating a new tenant -func (b *PulsarAdminTenantToolBuilder) handleTenantCreate(admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - tenant, err := request.RequireString("tenant") +func (b *PulsarAdminTenantToolBuilder) handleTenantCreate(admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + tenant, err := adapter.RequireString(request, "tenant") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get tenant name: %v", err)), nil + return adapter.NewErrorResult("Failed to get tenant name: %v", err), nil } - adminRoles, err := request.RequireStringSlice("adminRoles") + adminRoles, err := adapter.RequireStringSlice(request, "adminRoles") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get admin roles: %v", err)), nil + return adapter.NewErrorResult("Failed to get admin roles: %v", err), nil } - allowedClusters, err := request.RequireStringSlice("allowedClusters") + allowedClusters, err := adapter.RequireStringSlice(request, "allowedClusters") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get allowed clusters: %v", err)), nil + return adapter.NewErrorResult("Failed to get allowed clusters: %v", err), nil } // Create tenant data @@ -280,24 +280,24 @@ func (b *PulsarAdminTenantToolBuilder) handleTenantCreate(admin cmdutils.Client, return b.handleError("create tenant", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Tenant %s created successfully", tenant)), nil + return adapter.NewTextResult(fmt.Sprintf("Tenant %s created successfully", tenant)), nil } // handleTenantUpdate handles updating tenant configuration -func (b *PulsarAdminTenantToolBuilder) handleTenantUpdate(admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - tenant, err := request.RequireString("tenant") +func (b *PulsarAdminTenantToolBuilder) handleTenantUpdate(admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + tenant, err := adapter.RequireString(request, "tenant") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get tenant name: %v", err)), nil + return adapter.NewErrorResult("Failed to get tenant name: %v", err), nil } - adminRoles, err := request.RequireStringSlice("adminRoles") + adminRoles, err := adapter.RequireStringSlice(request, "adminRoles") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get admin roles: %v", err)), nil + return adapter.NewErrorResult("Failed to get admin roles: %v", err), nil } - allowedClusters, err := request.RequireStringSlice("allowedClusters") + allowedClusters, err := adapter.RequireStringSlice(request, "allowedClusters") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get allowed clusters: %v", err)), nil + return adapter.NewErrorResult("Failed to get allowed clusters: %v", err), nil } // Create tenant data @@ -313,14 +313,14 @@ func (b *PulsarAdminTenantToolBuilder) handleTenantUpdate(admin cmdutils.Client, return b.handleError("update tenant", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Tenant %s updated successfully", tenant)), nil + return adapter.NewTextResult(fmt.Sprintf("Tenant %s updated successfully", tenant)), nil } // handleTenantDelete handles deleting a tenant -func (b *PulsarAdminTenantToolBuilder) handleTenantDelete(admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - tenant, err := request.RequireString("tenant") +func (b *PulsarAdminTenantToolBuilder) handleTenantDelete(admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + tenant, err := adapter.RequireString(request, "tenant") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get tenant name: %v", err)), nil + return adapter.NewErrorResult("Failed to get tenant name: %v", err), nil } // Delete tenant @@ -329,5 +329,5 @@ func (b *PulsarAdminTenantToolBuilder) handleTenantDelete(admin cmdutils.Client, return b.handleError("delete tenant", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Tenant %s deleted successfully", tenant)), nil + return adapter.NewTextResult(fmt.Sprintf("Tenant %s deleted successfully", tenant)), nil } diff --git a/pkg/mcp/builders/pulsar/topic.go b/pkg/mcp/builders/pulsar/topic.go index 90430e1..073078f 100644 --- a/pkg/mcp/builders/pulsar/topic.go +++ b/pkg/mcp/builders/pulsar/topic.go @@ -22,10 +22,10 @@ import ( "strings" "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/pulsarctl/pkg/cmdutils" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" mcpCtx "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" ) @@ -60,7 +60,7 @@ func NewPulsarAdminTopicToolBuilder() *PulsarAdminTopicToolBuilder { // BuildTools builds the Pulsar Admin Topic tool list // This is the core method implementing the ToolBuilder interface -func (b *PulsarAdminTopicToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]server.ServerTool, error) { +func (b *PulsarAdminTopicToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]builders.ServerTool, error) { // Check features - return empty list if no required features are present if !b.HasAnyRequiredFeature(config.Features) { return nil, nil @@ -75,7 +75,7 @@ func (b *PulsarAdminTopicToolBuilder) BuildTools(_ context.Context, config build tool := b.buildTopicTool() handler := b.buildTopicHandler(config.ReadOnly) - return []server.ServerTool{ + return []builders.ServerTool{ { Tool: tool, Handler: handler, @@ -85,7 +85,7 @@ func (b *PulsarAdminTopicToolBuilder) BuildTools(_ context.Context, config build // buildTopicTool builds the Pulsar Admin Topic MCP tool definition // Migrated from the original tool definition logic -func (b *PulsarAdminTopicToolBuilder) buildTopicTool() mcp.Tool { +func (b *PulsarAdminTopicToolBuilder) buildTopicTool() *mcpsdk.Tool { toolDesc := "Manage Apache Pulsar topics. " + "Topics are the core messaging entities in Pulsar that store and transmit messages. " + "Pulsar supports two types of topics: persistent (durable storage with guaranteed delivery) " + @@ -121,57 +121,57 @@ func (b *PulsarAdminTopicToolBuilder) buildTopicTool() mcp.Tool { "- offload: Offload data from a topic to long-term storage\n" + "- offload-status: Check the status of data offloading for a topic" - return mcp.NewTool("pulsar_admin_topic", - mcp.WithDescription(toolDesc), - mcp.WithString("resource", mcp.Required(), - mcp.Description(resourceDesc), + return builders.NewTool("pulsar_admin_topic", + builders.WithDescription(toolDesc), + builders.WithString("resource", builders.Required(), + builders.Description(resourceDesc), ), - mcp.WithString("operation", mcp.Required(), - mcp.Description(operationDesc), + builders.WithString("operation", builders.Required(), + builders.Description(operationDesc), ), - mcp.WithString("topic", - mcp.Description("The fully qualified topic name (format: [persistent|non-persistent]://tenant/namespace/topic). "+ + builders.WithString("topic", + builders.Description("The fully qualified topic name (format: [persistent|non-persistent]://tenant/namespace/topic). "+ "Required for all operations except 'list'. "+ "For partitioned topics, reference the base topic name without the partition suffix. "+ "To operate on a specific partition, append -partition-N to the topic name."), ), - mcp.WithString("namespace", - mcp.Description("The namespace name in the format 'tenant/namespace'. "+ + builders.WithString("namespace", + builders.Description("The namespace name in the format 'tenant/namespace'. "+ "Required for the 'list' operation. "+ "A namespace is a logical grouping of topics within a tenant."), ), - mcp.WithNumber("partitions", - mcp.Description("The number of partitions for the topic. Required for 'create' and 'update' operations. "+ + builders.WithNumber("partitions", + builders.Description("The number of partitions for the topic. Required for 'create' and 'update' operations. "+ "Set to 0 for a non-partitioned topic. "+ "Partitioned topics provide higher throughput by dividing message traffic across multiple brokers. "+ "Each partition is an independent unit with its own retention and cursor positions."), ), - mcp.WithBoolean("force", - mcp.Description("Force operation even if it disrupts producers or consumers. Optional for 'delete' operation. "+ + builders.WithBoolean("force", + builders.Description("Force operation even if it disrupts producers or consumers. Optional for 'delete' operation. "+ "When true, all producers and consumers will be forcefully disconnected. "+ "Use with caution as it can interrupt active message processing."), ), - mcp.WithBoolean("non-partitioned", - mcp.Description("Operate on a non-partitioned topic. Optional for 'delete' operation. "+ + builders.WithBoolean("non-partitioned", + builders.Description("Operate on a non-partitioned topic. Optional for 'delete' operation. "+ "When true and operating on a partitioned topic name, only deletes the non-partitioned topic "+ "with the same name, if it exists."), ), - mcp.WithBoolean("partitioned", - mcp.Description("Get stats for a partitioned topic. Optional for 'stats' operation. "+ + builders.WithBoolean("partitioned", + builders.Description("Get stats for a partitioned topic. Optional for 'stats' operation. "+ "It has to be true if the topic is partitioned. Leave it empty or false for non-partitioned topic."), ), - mcp.WithBoolean("per-partition", - mcp.Description("Include per-partition stats. Optional for 'stats' operation. "+ + builders.WithBoolean("per-partition", + builders.Description("Include per-partition stats. Optional for 'stats' operation. "+ "When true, returns statistics for each partition separately. "+ "Requires 'partitioned' parameter to be true."), ), - mcp.WithString("config", - mcp.Description("JSON configuration for the topic. Required for 'update' operation. "+ + builders.WithString("config", + builders.Description("JSON configuration for the topic. Required for 'update' operation. "+ "Set various policies like retention, compaction, deduplication, etc. "+ "Use a JSON object format, e.g., '{\"deduplicationEnabled\": true, \"replication_clusters\": [\"us-west\", \"us-east\"]}'"), ), - mcp.WithString("messageId", - mcp.Description("Message ID for operations that require a position. Required for 'offload' operation. "+ + builders.WithString("messageId", + builders.Description("Message ID for operations that require a position. Required for 'offload' operation. "+ "Format is 'ledgerId:entryId' representing a position in the topic's message log. "+ "For offload operations, specifies the message up to which data should be moved to long-term storage."), ), @@ -180,17 +180,17 @@ func (b *PulsarAdminTopicToolBuilder) buildTopicTool() mcp.Tool { // buildTopicHandler builds the Pulsar Admin Topic handler function // Migrated from the original handler logic -func (b *PulsarAdminTopicToolBuilder) buildTopicHandler(readOnly bool) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicToolBuilder) buildTopicHandler(readOnly bool) func(context.Context, *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - resource, err := request.RequireString("resource") + resource, err := adapter.RequireString(request, "resource") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get resource: %v", err)), nil + return adapter.NewErrorResult("Failed to get resource: %v", err), nil } - operation, err := request.RequireString("operation") + operation, err := adapter.RequireString(request, "operation") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get operation: %v", err)), nil + return adapter.NewErrorResult("Failed to get operation: %v", err), nil } // Normalize parameters @@ -200,19 +200,19 @@ func (b *PulsarAdminTopicToolBuilder) buildTopicHandler(readOnly bool) func(cont // Validate write operations in read-only mode if readOnly && (operation == "create" || operation == "delete" || operation == "unload" || operation == "terminate" || operation == "compact" || operation == "update" || operation == "offload") { - return mcp.NewToolResultError("Write operations are not allowed in read-only mode"), nil + return adapter.NewErrorResult("Write operations are not allowed in read-only mode"), nil } // Get Pulsar session from context session := mcpCtx.GetPulsarSession(ctx) if session == nil { - return mcp.NewToolResultError("Pulsar session not found in context"), nil + return adapter.NewErrorResult("Pulsar session not found in context"), nil } // Create the admin client admin, err := session.GetAdminClient() if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get admin client: %v", err)), nil + return adapter.NewErrorResult("Failed to get admin client: %v", err), nil } // Dispatch based on resource and operation @@ -252,17 +252,17 @@ func (b *PulsarAdminTopicToolBuilder) buildTopicHandler(readOnly bool) func(cont case "offload-status": return b.handleTopicOffloadStatus(admin, request) default: - return mcp.NewToolResultError(fmt.Sprintf("Unknown topic operation: %s", operation)), nil + return adapter.NewErrorResult("Unknown topic operation: %s", operation), nil } case "topics": switch operation { case "list": return b.handleTopicsList(admin, request) default: - return mcp.NewToolResultError(fmt.Sprintf("Unknown topics operation: %s", operation)), nil + return adapter.NewErrorResult("Unknown topics operation: %s", operation), nil } default: - return mcp.NewToolResultError(fmt.Sprintf("Unknown resource: %s", resource)), nil + return adapter.NewErrorResult("Unknown resource: %s", resource), nil } } } @@ -270,38 +270,38 @@ func (b *PulsarAdminTopicToolBuilder) buildTopicHandler(readOnly bool) func(cont // Unified error handling and utility functions // handleError provides unified error handling -func (b *PulsarAdminTopicToolBuilder) handleError(operation string, err error) *mcp.CallToolResult { - return mcp.NewToolResultError(fmt.Sprintf("Failed to %s: %v", operation, err)) +func (b *PulsarAdminTopicToolBuilder) handleError(operation string, err error) *mcpsdk.CallToolResult { + return adapter.NewErrorResult("Failed to %s: %v", operation, err) } // marshalResponse provides unified JSON serialization for responses -func (b *PulsarAdminTopicToolBuilder) marshalResponse(data interface{}) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicToolBuilder) marshalResponse(data interface{}) (*mcpsdk.CallToolResult, error) { jsonBytes, err := json.Marshal(data) if err != nil { return b.handleError("marshal response", err), nil } - return mcp.NewToolResultText(string(jsonBytes)), nil + return adapter.NewTextResult(string(jsonBytes)), nil } // handleTopicsList lists all existing topics under the specified namespace -func (b *PulsarAdminTopicToolBuilder) handleTopicsList(admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicToolBuilder) handleTopicsList(admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - namespace, err := request.RequireString("namespace") + namespace, err := adapter.RequireString(request, "namespace") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'namespace' for topics.list: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'namespace' for topics.list: %v", err), nil } // Get namespace name namespaceName, err := utils.GetNamespaceName(namespace) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid namespace name '%s': %v", namespace, err)), nil + return adapter.NewErrorResult("Invalid namespace name '%s': %v", namespace, err), nil } // List topics partitionedTopics, nonPartitionedTopics, err := admin.Topics().List(*namespaceName) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list topics in namespace '%s': %v", - namespace, err)), nil + return adapter.NewErrorResult("Failed to list topics in namespace '%s': %v", + namespace, err), nil } // Format the output @@ -317,57 +317,57 @@ func (b *PulsarAdminTopicToolBuilder) handleTopicsList(admin cmdutils.Client, re } // handleTopicGet gets the metadata of an existing topic -func (b *PulsarAdminTopicToolBuilder) handleTopicGet(admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicToolBuilder) handleTopicGet(admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - topic, err := request.RequireString("topic") + topic, err := adapter.RequireString(request, "topic") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'topic' for topic.get: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'topic' for topic.get: %v", err), nil } // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid topic name '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Invalid topic name '%s': %v", topic, err), nil } // Get topic metadata metadata, err := admin.Topics().GetMetadata(*topicName) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get metadata for topic '%s': %v", - topic, err)), nil + return adapter.NewErrorResult("Failed to get metadata for topic '%s': %v", + topic, err), nil } return b.marshalResponse(metadata) } // handleTopicStats gets the stats for an existing topic -func (b *PulsarAdminTopicToolBuilder) handleTopicStats(admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicToolBuilder) handleTopicStats(admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - topic, err := request.RequireString("topic") + topic, err := adapter.RequireString(request, "topic") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'topic' for topic.stats: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'topic' for topic.stats: %v", err), nil } // Get optional parameters - partitioned := request.GetBool("partitioned", false) - perPartition := request.GetBool("per-partition", false) + partitioned := adapter.GetBool(request, "partitioned", false) + perPartition := adapter.GetBool(request, "per-partition", false) // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid topic name '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Invalid topic name '%s': %v", topic, err), nil } namespaceName, err := utils.GetNamespaceName(topicName.GetTenant() + "/" + topicName.GetNamespace()) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid namespace name: %v", err)), nil + return adapter.NewErrorResult("Invalid namespace name: %v", err), nil } // List topics to determine if this topic is partitioned partitionedTopics, nonPartitionedTopics, err := admin.Topics().List(*namespaceName) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list topics in namespace '%s': %v", - namespaceName, err)), nil + return adapter.NewErrorResult("Failed to list topics in namespace '%s': %v", + namespaceName, err), nil } if slices.Contains(partitionedTopics, topicName.String()) { @@ -382,16 +382,16 @@ func (b *PulsarAdminTopicToolBuilder) handleTopicStats(admin cmdutils.Client, re // Get partitioned topic stats stats, err := admin.Topics().GetPartitionedStats(*topicName, perPartition) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get stats for partitioned topic '%s': %v", - topic, err)), nil + return adapter.NewErrorResult("Failed to get stats for partitioned topic '%s': %v", + topic, err), nil } data = stats } else { // Get topic stats stats, err := admin.Topics().GetStats(*topicName) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get stats for topic '%s': %v", - topic, err)), nil + return adapter.NewErrorResult("Failed to get stats for topic '%s': %v", + topic, err), nil } data = stats } @@ -400,90 +400,90 @@ func (b *PulsarAdminTopicToolBuilder) handleTopicStats(admin cmdutils.Client, re } // handleTopicLookup looks up the owner broker of a topic -func (b *PulsarAdminTopicToolBuilder) handleTopicLookup(admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicToolBuilder) handleTopicLookup(admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - topic, err := request.RequireString("topic") + topic, err := adapter.RequireString(request, "topic") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'topic' for topic.lookup: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'topic' for topic.lookup: %v", err), nil } // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid topic name '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Invalid topic name '%s': %v", topic, err), nil } // Lookup topic lookup, err := admin.Topics().Lookup(*topicName) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to lookup topic '%s': %v", - topic, err)), nil + return adapter.NewErrorResult("Failed to lookup topic '%s': %v", + topic, err), nil } return b.marshalResponse(lookup) } // handleTopicCreate creates a topic with the specified number of partitions -func (b *PulsarAdminTopicToolBuilder) handleTopicCreate(admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicToolBuilder) handleTopicCreate(admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - topic, err := request.RequireString("topic") + topic, err := adapter.RequireString(request, "topic") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'topic' for topic.create: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'topic' for topic.create: %v", err), nil } - partitions, err := request.RequireFloat("partitions") + partitions, err := adapter.RequireFloat(request, "partitions") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'partitions' for topic.create: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'partitions' for topic.create: %v", err), nil } // Validate partitions if partitions < 0 { - return mcp.NewToolResultError("Invalid partitions number: must be non-negative. Use 0 for a non-partitioned topic."), nil + return adapter.NewErrorResult("Invalid partitions number: must be non-negative. Use 0 for a non-partitioned topic."), nil } // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid topic name '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Invalid topic name '%s': %v", topic, err), nil } // Create topic err = admin.Topics().Create(*topicName, int(partitions)) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to create topic '%s' with %d partitions: %v", - topic, int(partitions), err)), nil + return adapter.NewErrorResult("Failed to create topic '%s' with %d partitions: %v", + topic, int(partitions), err), nil } if int(partitions) == 0 { - return mcp.NewToolResultText(fmt.Sprintf("Successfully created non-partitioned topic '%s'", + return adapter.NewTextResult(fmt.Sprintf("Successfully created non-partitioned topic '%s'", topicName.String())), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully created topic '%s' with %d partitions", + return adapter.NewTextResult(fmt.Sprintf("Successfully created topic '%s' with %d partitions", topicName.String(), int(partitions))), nil } // handleTopicDelete deletes a topic -func (b *PulsarAdminTopicToolBuilder) handleTopicDelete(admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicToolBuilder) handleTopicDelete(admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - topic, err := request.RequireString("topic") + topic, err := adapter.RequireString(request, "topic") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'topic' for topic.delete: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'topic' for topic.delete: %v", err), nil } // Get optional parameters - force := request.GetBool("force", false) - nonPartitioned := request.GetBool("non-partitioned", false) + force := adapter.GetBool(request, "force", false) + nonPartitioned := adapter.GetBool(request, "non-partitioned", false) // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid topic name '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Invalid topic name '%s': %v", topic, err), nil } // Delete topic err = admin.Topics().Delete(*topicName, force, nonPartitioned) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to delete topic '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Failed to delete topic '%s': %v", topic, err), nil } forceStr := "" @@ -496,195 +496,195 @@ func (b *PulsarAdminTopicToolBuilder) handleTopicDelete(admin cmdutils.Client, r nonPartitionedStr = " (non-partitioned)" } - return mcp.NewToolResultText(fmt.Sprintf("Successfully deleted topic '%s'%s%s", + return adapter.NewTextResult(fmt.Sprintf("Successfully deleted topic '%s'%s%s", topicName.String(), forceStr, nonPartitionedStr)), nil } // handleTopicUnload unloads a topic -func (b *PulsarAdminTopicToolBuilder) handleTopicUnload(admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicToolBuilder) handleTopicUnload(admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - topic, err := request.RequireString("topic") + topic, err := adapter.RequireString(request, "topic") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'topic' for topic.unload: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'topic' for topic.unload: %v", err), nil } // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid topic name '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Invalid topic name '%s': %v", topic, err), nil } // Unload topic err = admin.Topics().Unload(*topicName) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to unload topic '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Failed to unload topic '%s': %v", topic, err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully unloaded topic '%s'", topicName.String())), nil + return adapter.NewTextResult(fmt.Sprintf("Successfully unloaded topic '%s'", topicName.String())), nil } // handleTopicTerminate terminates a topic -func (b *PulsarAdminTopicToolBuilder) handleTopicTerminate(admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicToolBuilder) handleTopicTerminate(admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - topic, err := request.RequireString("topic") + topic, err := adapter.RequireString(request, "topic") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'topic' for topic.terminate: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'topic' for topic.terminate: %v", err), nil } // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid topic name '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Invalid topic name '%s': %v", topic, err), nil } // Terminate topic messageID, err := admin.Topics().Terminate(*topicName) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to terminate topic '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Failed to terminate topic '%s': %v", topic, err), nil } // Convert message ID to string msgIDStr := fmt.Sprintf("%d:%d", messageID.LedgerID, messageID.EntryID) - return mcp.NewToolResultText(fmt.Sprintf("Successfully terminated topic '%s' at message %s. "+ + return adapter.NewTextResult(fmt.Sprintf("Successfully terminated topic '%s' at message %s. "+ "No more messages can be published to this topic.", topicName.String(), msgIDStr)), nil } // handleTopicCompact triggers compaction on a topic -func (b *PulsarAdminTopicToolBuilder) handleTopicCompact(admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicToolBuilder) handleTopicCompact(admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - topic, err := request.RequireString("topic") + topic, err := adapter.RequireString(request, "topic") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'topic' for topic.compact: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'topic' for topic.compact: %v", err), nil } // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid topic name '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Invalid topic name '%s': %v", topic, err), nil } // Compact topic err = admin.Topics().Compact(*topicName) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to trigger compaction for topic '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Failed to trigger compaction for topic '%s': %v", topic, err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully triggered compaction for topic '%s'. "+ + return adapter.NewTextResult(fmt.Sprintf("Successfully triggered compaction for topic '%s'. "+ "Run 'topic.status' to check compaction status.", topicName.String())), nil } // handleTopicInternalStats gets the internal stats for a topic -func (b *PulsarAdminTopicToolBuilder) handleTopicInternalStats(admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicToolBuilder) handleTopicInternalStats(admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - topic, err := request.RequireString("topic") + topic, err := adapter.RequireString(request, "topic") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'topic' for topic.internal-stats: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'topic' for topic.internal-stats: %v", err), nil } // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid topic name '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Invalid topic name '%s': %v", topic, err), nil } // Get internal stats stats, err := admin.Topics().GetInternalStats(*topicName) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get internal stats for topic '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Failed to get internal stats for topic '%s': %v", topic, err), nil } return b.marshalResponse(stats) } // handleTopicInternalInfo gets the internal info for a topic -func (b *PulsarAdminTopicToolBuilder) handleTopicInternalInfo(admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicToolBuilder) handleTopicInternalInfo(admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - topic, err := request.RequireString("topic") + topic, err := adapter.RequireString(request, "topic") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'topic' for topic.internal-info: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'topic' for topic.internal-info: %v", err), nil } // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid topic name '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Invalid topic name '%s': %v", topic, err), nil } // Get internal info info, err := admin.Topics().GetInternalInfo(*topicName) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get internal info for topic '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Failed to get internal info for topic '%s': %v", topic, err), nil } return b.marshalResponse(info) } // handleTopicBundleRange gets the bundle range of a topic -func (b *PulsarAdminTopicToolBuilder) handleTopicBundleRange(admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicToolBuilder) handleTopicBundleRange(admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - topic, err := request.RequireString("topic") + topic, err := adapter.RequireString(request, "topic") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'topic' for topic.bundle-range: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'topic' for topic.bundle-range: %v", err), nil } // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid topic name '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Invalid topic name '%s': %v", topic, err), nil } // Get bundle range bundle, err := admin.Topics().GetBundleRange(*topicName) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get bundle range for topic '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Failed to get bundle range for topic '%s': %v", topic, err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Bundle range for topic '%s': %s", topicName.String(), bundle)), nil + return adapter.NewTextResult(fmt.Sprintf("Bundle range for topic '%s': %s", topicName.String(), bundle)), nil } // handleTopicLastMessageID gets the last message ID of a topic -func (b *PulsarAdminTopicToolBuilder) handleTopicLastMessageID(admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicToolBuilder) handleTopicLastMessageID(admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - topic, err := request.RequireString("topic") + topic, err := adapter.RequireString(request, "topic") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'topic' for topic.last-message-id: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'topic' for topic.last-message-id: %v", err), nil } // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid topic name '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Invalid topic name '%s': %v", topic, err), nil } // Get last message ID messageID, err := admin.Topics().GetLastMessageID(*topicName) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get last message ID for topic '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Failed to get last message ID for topic '%s': %v", topic, err), nil } return b.marshalResponse(messageID) } // handleTopicStatus gets the status of a topic -func (b *PulsarAdminTopicToolBuilder) handleTopicStatus(admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicToolBuilder) handleTopicStatus(admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - topic, err := request.RequireString("topic") + topic, err := adapter.RequireString(request, "topic") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'topic' for topic.status: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'topic' for topic.status: %v", err), nil } // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid topic name '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Invalid topic name '%s': %v", topic, err), nil } // Get topic metadata for status check metadata, err := admin.Topics().GetMetadata(*topicName) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get status for topic '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Failed to get status for topic '%s': %v", topic, err), nil } // Create status object with available information @@ -700,57 +700,57 @@ func (b *PulsarAdminTopicToolBuilder) handleTopicStatus(admin cmdutils.Client, r } // handleTopicUpdate updates a topic configuration -func (b *PulsarAdminTopicToolBuilder) handleTopicUpdate(admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicToolBuilder) handleTopicUpdate(admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - topic, err := request.RequireString("topic") + topic, err := adapter.RequireString(request, "topic") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'topic' for topic.update: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'topic' for topic.update: %v", err), nil } - partitions, err := request.RequireFloat("partitions") + partitions, err := adapter.RequireFloat(request, "partitions") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'partitions' for topic.update: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'partitions' for topic.update: %v", err), nil } // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid topic name '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Invalid topic name '%s': %v", topic, err), nil } err = admin.Topics().Update(*topicName, int(partitions)) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to update topic '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Failed to update topic '%s': %v", topic, err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully updated topic '%s' partitions to %d", + return adapter.NewTextResult(fmt.Sprintf("Successfully updated topic '%s' partitions to %d", topicName.String(), int(partitions))), nil } // handleTopicOffload offloads data from a topic to long-term storage -func (b *PulsarAdminTopicToolBuilder) handleTopicOffload(admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicToolBuilder) handleTopicOffload(admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - topic, err := request.RequireString("topic") + topic, err := adapter.RequireString(request, "topic") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'topic' for topic.offload: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'topic' for topic.offload: %v", err), nil } - messageIDStr, err := request.RequireString("messageId") + messageIDStr, err := adapter.RequireString(request, "messageId") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'messageId' for topic.offload: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'messageId' for topic.offload: %v", err), nil } // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid topic name '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Invalid topic name '%s': %v", topic, err), nil } // Parse message ID from format "ledgerId:entryId" var ledgerID, entryID int64 if _, err := fmt.Sscanf(messageIDStr, "%d:%d", &ledgerID, &entryID); err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid message ID format (expected 'ledgerId:entryId'): %v. "+ - "Valid examples: '123:456'", err)), nil + return adapter.NewErrorResult("Invalid message ID format (expected 'ledgerId:entryId'): %v. "+ + "Valid examples: '123:456'", err), nil } // Create MessageID object @@ -762,32 +762,32 @@ func (b *PulsarAdminTopicToolBuilder) handleTopicOffload(admin cmdutils.Client, // Offload topic err = admin.Topics().Offload(*topicName, messageID) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to trigger offload for topic '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Failed to trigger offload for topic '%s': %v", topic, err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully triggered offload for topic '%s' up to message %s. "+ + return adapter.NewTextResult(fmt.Sprintf("Successfully triggered offload for topic '%s' up to message %s. "+ "Use 'topic.offload-status' to check the offload progress.", topicName.String(), messageIDStr)), nil } // handleTopicOffloadStatus checks the status of data offloading for a topic -func (b *PulsarAdminTopicToolBuilder) handleTopicOffloadStatus(admin cmdutils.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicToolBuilder) handleTopicOffloadStatus(admin cmdutils.Client, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get required parameters - topic, err := request.RequireString("topic") + topic, err := adapter.RequireString(request, "topic") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'topic' for topic.offload-status: %v", err)), nil + return adapter.NewErrorResult("Missing required parameter 'topic' for topic.offload-status: %v", err), nil } // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid topic name '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Invalid topic name '%s': %v", topic, err), nil } // Get offload status status, err := admin.Topics().OffloadStatus(*topicName) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get offload status for topic '%s': %v", topic, err)), nil + return adapter.NewErrorResult("Failed to get offload status for topic '%s': %v", topic, err), nil } return b.marshalResponse(status) diff --git a/pkg/mcp/builders/pulsar/topic_policy.go b/pkg/mcp/builders/pulsar/topic_policy.go index 065caa6..a1f1403 100644 --- a/pkg/mcp/builders/pulsar/topic_policy.go +++ b/pkg/mcp/builders/pulsar/topic_policy.go @@ -22,10 +22,10 @@ import ( "strings" "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/pulsarctl/pkg/cmdutils" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" mcpCtx "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" ) @@ -58,7 +58,7 @@ func NewPulsarAdminTopicPolicyToolBuilder() *PulsarAdminTopicPolicyToolBuilder { } // BuildTools builds the Pulsar admin topic policy tool list -func (b *PulsarAdminTopicPolicyToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]server.ServerTool, error) { +func (b *PulsarAdminTopicPolicyToolBuilder) BuildTools(_ context.Context, config builders.ToolBuildConfig) ([]builders.ServerTool, error) { // Check features - return empty list if no required features are present if !b.HasAnyRequiredFeature(config.Features) { return nil, nil @@ -73,7 +73,7 @@ func (b *PulsarAdminTopicPolicyToolBuilder) BuildTools(_ context.Context, config tool := b.buildTopicPolicyTool() handler := b.buildTopicPolicyHandler(config.ReadOnly) - return []server.ServerTool{ + return []builders.ServerTool{ { Tool: tool, Handler: handler, @@ -82,7 +82,7 @@ func (b *PulsarAdminTopicPolicyToolBuilder) BuildTools(_ context.Context, config } // buildTopicPolicyTool builds the Pulsar Admin Topic Policy MCP tool definition -func (b *PulsarAdminTopicPolicyToolBuilder) buildTopicPolicyTool() mcp.Tool { +func (b *PulsarAdminTopicPolicyToolBuilder) buildTopicPolicyTool() *mcpsdk.Tool { toolDesc := "Manage Pulsar topic policies including retention, TTL, compaction, and subscription policies. " + "This tool provides functionality to get, set, and remove various topic-level policies in Apache Pulsar." @@ -100,29 +100,29 @@ func (b *PulsarAdminTopicPolicyToolBuilder) buildTopicPolicyTool() mcp.Tool { "- set_subscription_types: Set allowed subscription types for a topic\n" + "- remove_subscription_types: Remove subscription types restriction for a topic" - return mcp.NewTool("pulsar_admin_topic_policy", - mcp.WithDescription(toolDesc), - mcp.WithString("operation", mcp.Required(), - mcp.Description(operationDesc), + return builders.NewTool("pulsar_admin_topic_policy", + builders.WithDescription(toolDesc), + builders.WithString("operation", builders.Required(), + builders.Description(operationDesc), ), - mcp.WithString("topic", mcp.Required(), - mcp.Description("Topic name in format 'persistent://tenant/namespace/topic' or 'tenant/namespace/topic'"), + builders.WithString("topic", builders.Required(), + builders.Description("Topic name in format 'persistent://tenant/namespace/topic' or 'tenant/namespace/topic'"), ), - mcp.WithString("retention_size", - mcp.Description("Retention size policy (e.g., '100MB', '1GB') - used with retention operations"), + builders.WithString("retention_size", + builders.Description("Retention size policy (e.g., '100MB', '1GB') - used with retention operations"), ), - mcp.WithString("retention_time", - mcp.Description("Retention time policy (e.g., '1d', '24h', '1440m') - used with retention operations"), + builders.WithString("retention_time", + builders.Description("Retention time policy (e.g., '1d', '24h', '1440m') - used with retention operations"), ), - mcp.WithNumber("ttl_seconds", - mcp.Description("TTL in seconds - used with TTL operations"), + builders.WithNumber("ttl_seconds", + builders.Description("TTL in seconds - used with TTL operations"), ), - mcp.WithNumber("compaction_threshold", - mcp.Description("Compaction threshold in bytes - used with compaction operations"), + builders.WithNumber("compaction_threshold", + builders.Description("Compaction threshold in bytes - used with compaction operations"), ), - mcp.WithArray("subscription_types", - mcp.Description("List of allowed subscription types - used with subscription type operations"), - mcp.Items( + builders.WithArray("subscription_types", + builders.Description("List of allowed subscription types - used with subscription type operations"), + builders.Items( map[string]interface{}{ "type": "string", "description": "subscription type: Exclusive, Shared, Failover, Key_Shared", @@ -133,12 +133,12 @@ func (b *PulsarAdminTopicPolicyToolBuilder) buildTopicPolicyTool() mcp.Tool { } // buildTopicPolicyHandler builds the Pulsar Admin Topic Policy handler function -func (b *PulsarAdminTopicPolicyToolBuilder) buildTopicPolicyHandler(readOnly bool) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicPolicyToolBuilder) buildTopicPolicyHandler(readOnly bool) func(context.Context, *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get Pulsar session from context session := mcpCtx.GetPulsarSession(ctx) if session == nil { - return mcp.NewToolResultError("Pulsar session not found in context"), nil + return adapter.NewErrorResult("Pulsar session not found in context"), nil } client, err := session.GetAdminClient() @@ -147,14 +147,14 @@ func (b *PulsarAdminTopicPolicyToolBuilder) buildTopicPolicyHandler(readOnly boo } // Get required parameters - operation, err := request.RequireString("operation") + operation, err := adapter.RequireString(request, "operation") if err != nil { - return mcp.NewToolResultError("Missing required operation parameter"), nil + return adapter.NewErrorResult("Missing required operation parameter"), nil } - topic, err := request.RequireString("topic") + topic, err := adapter.RequireString(request, "topic") if err != nil { - return mcp.NewToolResultError("Missing required topic parameter"), nil + return adapter.NewErrorResult("Missing required topic parameter"), nil } // Check write operation permissions @@ -169,7 +169,7 @@ func (b *PulsarAdminTopicPolicyToolBuilder) buildTopicPolicyHandler(readOnly boo } if isWriteOp && readOnly { - return mcp.NewToolResultError("Write operations not allowed in read-only mode"), nil + return adapter.NewErrorResult("Write operations not allowed in read-only mode"), nil } // Handle operations @@ -199,26 +199,26 @@ func (b *PulsarAdminTopicPolicyToolBuilder) buildTopicPolicyHandler(readOnly boo case "remove_subscription_types": return b.handleRemoveTopicSubscriptionTypes(client, topic) default: - return mcp.NewToolResultError(fmt.Sprintf("Unsupported operation: %s", operation)), nil + return adapter.NewErrorResult("Unsupported operation: %s", operation), nil } } } // Utility functions -func (b *PulsarAdminTopicPolicyToolBuilder) handleError(operation string, err error) *mcp.CallToolResult { - return mcp.NewToolResultError(fmt.Sprintf("Failed to %s: %v", operation, err)) +func (b *PulsarAdminTopicPolicyToolBuilder) handleError(operation string, err error) *mcpsdk.CallToolResult { + return adapter.NewErrorResult("Failed to %s: %v", operation, err) } -func (b *PulsarAdminTopicPolicyToolBuilder) marshalResponse(data interface{}) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicPolicyToolBuilder) marshalResponse(data interface{}) (*mcpsdk.CallToolResult, error) { jsonBytes, err := json.Marshal(data) if err != nil { return b.handleError("marshal response", err), nil } - return mcp.NewToolResultText(string(jsonBytes)), nil + return adapter.NewTextResult(string(jsonBytes)), nil } // Topic policy operation handlers -func (b *PulsarAdminTopicPolicyToolBuilder) handleGetTopicRetention(client cmdutils.Client, topic string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicPolicyToolBuilder) handleGetTopicRetention(client cmdutils.Client, topic string) (*mcpsdk.CallToolResult, error) { // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { @@ -233,7 +233,7 @@ func (b *PulsarAdminTopicPolicyToolBuilder) handleGetTopicRetention(client cmdut // If no retention policy is defined if retention == nil { - return mcp.NewToolResultText(fmt.Sprintf("No retention policy found for topic %s", topicName.String())), nil + return adapter.NewTextResult(fmt.Sprintf("No retention policy found for topic %s", topicName.String())), nil } // Format the output @@ -251,11 +251,11 @@ func (b *PulsarAdminTopicPolicyToolBuilder) handleGetTopicRetention(client cmdut retentionSize = fmt.Sprintf("%d MB", retention.RetentionSizeInMB) } - return mcp.NewToolResultText(fmt.Sprintf("Retention policy for topic %s: %s and %s", + return adapter.NewTextResult(fmt.Sprintf("Retention policy for topic %s: %s and %s", topicName.String(), retentionTime, retentionSize)), nil } -func (b *PulsarAdminTopicPolicyToolBuilder) handleSetTopicRetention(client cmdutils.Client, topic string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicPolicyToolBuilder) handleSetTopicRetention(client cmdutils.Client, topic string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { @@ -267,18 +267,18 @@ func (b *PulsarAdminTopicPolicyToolBuilder) handleSetTopicRetention(client cmdut var retentionSizeInMB int64 = -1 // /nolint:revive - if retentionTime := request.GetString("retention_time", ""); retentionTime != "" { + if retentionTime := adapter.GetString(request, "retention_time", ""); retentionTime != "" { if parsed, err := b.parseRetentionTime(retentionTime); err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid retention time format: %v", err)), nil + return adapter.NewErrorResult("Invalid retention time format: %v", err), nil } else { retentionTimeInMinutes = parsed } } // /nolint:revive - if retentionSize := request.GetString("retention_size", ""); retentionSize != "" { + if retentionSize := adapter.GetString(request, "retention_size", ""); retentionSize != "" { if parsed, err := b.parseRetentionSize(retentionSize); err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid retention size format: %v", err)), nil + return adapter.NewErrorResult("Invalid retention size format: %v", err), nil } else { retentionSizeInMB = parsed } @@ -296,10 +296,10 @@ func (b *PulsarAdminTopicPolicyToolBuilder) handleSetTopicRetention(client cmdut return b.handleError("set topic retention policy", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Retention policy set for topic %s", topicName.String())), nil + return adapter.NewTextResult(fmt.Sprintf("Retention policy set for topic %s", topicName.String())), nil } -func (b *PulsarAdminTopicPolicyToolBuilder) handleRemoveTopicRetention(client cmdutils.Client, topic string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicPolicyToolBuilder) handleRemoveTopicRetention(client cmdutils.Client, topic string) (*mcpsdk.CallToolResult, error) { // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { @@ -312,10 +312,10 @@ func (b *PulsarAdminTopicPolicyToolBuilder) handleRemoveTopicRetention(client cm return b.handleError("remove topic retention policy", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Retention policy removed for topic %s", topicName.String())), nil + return adapter.NewTextResult(fmt.Sprintf("Retention policy removed for topic %s", topicName.String())), nil } -func (b *PulsarAdminTopicPolicyToolBuilder) handleGetTopicTTL(client cmdutils.Client, topic string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicPolicyToolBuilder) handleGetTopicTTL(client cmdutils.Client, topic string) (*mcpsdk.CallToolResult, error) { // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { @@ -330,13 +330,13 @@ func (b *PulsarAdminTopicPolicyToolBuilder) handleGetTopicTTL(client cmdutils.Cl // Check if TTL is set if ttl == 0 { - return mcp.NewToolResultText(fmt.Sprintf("Message TTL is not configured for topic %s", topicName.String())), nil + return adapter.NewTextResult(fmt.Sprintf("Message TTL is not configured for topic %s", topicName.String())), nil } - return mcp.NewToolResultText(fmt.Sprintf("Message TTL for topic %s is %d seconds", topicName.String(), ttl)), nil + return adapter.NewTextResult(fmt.Sprintf("Message TTL for topic %s is %d seconds", topicName.String(), ttl)), nil } -func (b *PulsarAdminTopicPolicyToolBuilder) handleSetTopicTTL(client cmdutils.Client, topic string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicPolicyToolBuilder) handleSetTopicTTL(client cmdutils.Client, topic string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { @@ -344,13 +344,13 @@ func (b *PulsarAdminTopicPolicyToolBuilder) handleSetTopicTTL(client cmdutils.Cl } // Get TTL seconds parameter - ttlSeconds, err := request.RequireFloat("ttl_seconds") + ttlSeconds, err := adapter.RequireFloat(request, "ttl_seconds") if err != nil { - return mcp.NewToolResultError("Missing required parameter 'ttl_seconds'"), nil + return adapter.NewErrorResult("Missing required parameter 'ttl_seconds'"), nil } if ttlSeconds < 0 { - return mcp.NewToolResultError("TTL seconds must be non-negative"), nil + return adapter.NewErrorResult("TTL seconds must be non-negative"), nil } // Set message TTL @@ -360,13 +360,13 @@ func (b *PulsarAdminTopicPolicyToolBuilder) handleSetTopicTTL(client cmdutils.Cl } if ttlSeconds == 0 { - return mcp.NewToolResultText(fmt.Sprintf("Message TTL disabled for topic %s", topicName.String())), nil + return adapter.NewTextResult(fmt.Sprintf("Message TTL disabled for topic %s", topicName.String())), nil } - return mcp.NewToolResultText(fmt.Sprintf("Message TTL set to %d seconds for topic %s", int(ttlSeconds), topicName.String())), nil + return adapter.NewTextResult(fmt.Sprintf("Message TTL set to %d seconds for topic %s", int(ttlSeconds), topicName.String())), nil } -func (b *PulsarAdminTopicPolicyToolBuilder) handleRemoveTopicTTL(client cmdutils.Client, topic string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicPolicyToolBuilder) handleRemoveTopicTTL(client cmdutils.Client, topic string) (*mcpsdk.CallToolResult, error) { // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { @@ -379,10 +379,10 @@ func (b *PulsarAdminTopicPolicyToolBuilder) handleRemoveTopicTTL(client cmdutils return b.handleError("remove topic message TTL", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Message TTL removed for topic %s", topicName.String())), nil + return adapter.NewTextResult(fmt.Sprintf("Message TTL removed for topic %s", topicName.String())), nil } -func (b *PulsarAdminTopicPolicyToolBuilder) handleGetTopicCompaction(client cmdutils.Client, topic string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicPolicyToolBuilder) handleGetTopicCompaction(client cmdutils.Client, topic string) (*mcpsdk.CallToolResult, error) { // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { @@ -397,14 +397,14 @@ func (b *PulsarAdminTopicPolicyToolBuilder) handleGetTopicCompaction(client cmdu // Format the result if threshold == 0 { - return mcp.NewToolResultText(fmt.Sprintf("Automatic compaction is disabled for topic %s", topicName.String())), nil + return adapter.NewTextResult(fmt.Sprintf("Automatic compaction is disabled for topic %s", topicName.String())), nil } - return mcp.NewToolResultText(fmt.Sprintf("The compaction threshold of the topic %s is %d byte(s)", + return adapter.NewTextResult(fmt.Sprintf("The compaction threshold of the topic %s is %d byte(s)", topicName.String(), threshold)), nil } -func (b *PulsarAdminTopicPolicyToolBuilder) handleSetTopicCompaction(client cmdutils.Client, topic string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicPolicyToolBuilder) handleSetTopicCompaction(client cmdutils.Client, topic string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { @@ -412,13 +412,13 @@ func (b *PulsarAdminTopicPolicyToolBuilder) handleSetTopicCompaction(client cmdu } // Get compaction threshold parameter - thresholdNum, err := request.RequireFloat("compaction_threshold") + thresholdNum, err := adapter.RequireFloat(request, "compaction_threshold") if err != nil { - return mcp.NewToolResultError("Missing required parameter 'compaction_threshold'"), nil + return adapter.NewErrorResult("Missing required parameter 'compaction_threshold'"), nil } if thresholdNum < 0 { - return mcp.NewToolResultError("Compaction threshold must be non-negative"), nil + return adapter.NewErrorResult("Compaction threshold must be non-negative"), nil } threshold := int64(thresholdNum) @@ -430,13 +430,13 @@ func (b *PulsarAdminTopicPolicyToolBuilder) handleSetTopicCompaction(client cmdu } if threshold == 0 { - return mcp.NewToolResultText(fmt.Sprintf("Automatic compaction disabled for topic %s", topicName.String())), nil + return adapter.NewTextResult(fmt.Sprintf("Automatic compaction disabled for topic %s", topicName.String())), nil } - return mcp.NewToolResultText(fmt.Sprintf("Compaction threshold set to %d bytes for topic %s", threshold, topicName.String())), nil + return adapter.NewTextResult(fmt.Sprintf("Compaction threshold set to %d bytes for topic %s", threshold, topicName.String())), nil } -func (b *PulsarAdminTopicPolicyToolBuilder) handleRemoveTopicCompaction(client cmdutils.Client, topic string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicPolicyToolBuilder) handleRemoveTopicCompaction(client cmdutils.Client, topic string) (*mcpsdk.CallToolResult, error) { // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { @@ -449,10 +449,10 @@ func (b *PulsarAdminTopicPolicyToolBuilder) handleRemoveTopicCompaction(client c return b.handleError("remove topic compaction threshold", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Compaction threshold removed for topic %s", topicName.String())), nil + return adapter.NewTextResult(fmt.Sprintf("Compaction threshold removed for topic %s", topicName.String())), nil } -func (b *PulsarAdminTopicPolicyToolBuilder) handleGetTopicSubscriptionTypes(client cmdutils.Client, topic string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicPolicyToolBuilder) handleGetTopicSubscriptionTypes(client cmdutils.Client, topic string) (*mcpsdk.CallToolResult, error) { // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { @@ -475,7 +475,7 @@ func (b *PulsarAdminTopicPolicyToolBuilder) handleGetTopicSubscriptionTypes(clie } if len(subscriptionTypes) == 0 { - return mcp.NewToolResultText(fmt.Sprintf("No subscription type restrictions configured for topic %s (all types allowed)", topicName.String())), nil + return adapter.NewTextResult(fmt.Sprintf("No subscription type restrictions configured for topic %s (all types allowed)", topicName.String())), nil } return b.marshalResponse(map[string]interface{}{ @@ -485,11 +485,11 @@ func (b *PulsarAdminTopicPolicyToolBuilder) handleGetTopicSubscriptionTypes(clie } // Fallback: API not available in current version - return mcp.NewToolResultError("Subscription types policy management is not available in the current pulsarctl API version. " + + return adapter.NewErrorResult("Subscription types policy management is not available in the current pulsarctl API version. " + "This feature may require a newer version of Pulsar or pulsarctl."), nil } -func (b *PulsarAdminTopicPolicyToolBuilder) handleSetTopicSubscriptionTypes(client cmdutils.Client, topic string, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicPolicyToolBuilder) handleSetTopicSubscriptionTypes(client cmdutils.Client, topic string, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { @@ -497,9 +497,9 @@ func (b *PulsarAdminTopicPolicyToolBuilder) handleSetTopicSubscriptionTypes(clie } // Get subscription types parameter - subscriptionTypes, err := request.RequireStringSlice("subscription_types") + subscriptionTypes, err := adapter.RequireStringSlice(request, "subscription_types") if err != nil { - return mcp.NewToolResultError("Missing required parameter 'subscription_types'. " + + return adapter.NewErrorResult("Missing required parameter 'subscription_types'. " + "Please provide an array of subscription types: Exclusive, Shared, Failover, Key_Shared"), nil } @@ -514,13 +514,13 @@ func (b *PulsarAdminTopicPolicyToolBuilder) handleSetTopicSubscriptionTypes(clie var validatedTypes []string for _, subType := range subscriptionTypes { if !validTypes[subType] { - return mcp.NewToolResultError(fmt.Sprintf("Invalid subscription type: %s. Valid types are: Exclusive, Shared, Failover, Key_Shared", subType)), nil + return adapter.NewErrorResult("Invalid subscription type: %s. Valid types are: Exclusive, Shared, Failover, Key_Shared", subType), nil } validatedTypes = append(validatedTypes, subType) } if len(validatedTypes) == 0 { - return mcp.NewToolResultError("At least one valid subscription type must be specified"), nil + return adapter.NewErrorResult("At least one valid subscription type must be specified"), nil } // Check if the API supports subscription types management @@ -537,16 +537,16 @@ func (b *PulsarAdminTopicPolicyToolBuilder) handleSetTopicSubscriptionTypes(clie return b.handleError("set topic subscription types", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Subscription types set for topic %s: %s", + return adapter.NewTextResult(fmt.Sprintf("Subscription types set for topic %s: %s", topicName.String(), strings.Join(validatedTypes, ", "))), nil } // Fallback: API not available in current version - return mcp.NewToolResultError("Subscription types policy management is not available in the current pulsarctl API version. " + + return adapter.NewErrorResult("Subscription types policy management is not available in the current pulsarctl API version. " + "This feature may require a newer version of Pulsar or pulsarctl."), nil } -func (b *PulsarAdminTopicPolicyToolBuilder) handleRemoveTopicSubscriptionTypes(client cmdutils.Client, topic string) (*mcp.CallToolResult, error) { +func (b *PulsarAdminTopicPolicyToolBuilder) handleRemoveTopicSubscriptionTypes(client cmdutils.Client, topic string) (*mcpsdk.CallToolResult, error) { // Get topic name topicName, err := utils.GetTopicName(topic) if err != nil { @@ -567,11 +567,11 @@ func (b *PulsarAdminTopicPolicyToolBuilder) handleRemoveTopicSubscriptionTypes(c return b.handleError("remove topic subscription types policy", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Subscription types policy removed for topic %s (all types now allowed)", topicName.String())), nil + return adapter.NewTextResult(fmt.Sprintf("Subscription types policy removed for topic %s (all types now allowed)", topicName.String())), nil } // Fallback: API not available in current version - return mcp.NewToolResultError("Subscription types policy management is not available in the current pulsarctl API version. " + + return adapter.NewErrorResult("Subscription types policy management is not available in the current pulsarctl API version. " + "This feature may require a newer version of Pulsar or pulsarctl."), nil } diff --git a/pkg/mcp/builders/registry.go b/pkg/mcp/builders/registry.go index 03c3726..b3b7f6d 100644 --- a/pkg/mcp/builders/registry.go +++ b/pkg/mcp/builders/registry.go @@ -19,8 +19,6 @@ import ( "fmt" "sort" "sync" - - "github.com/mark3labs/mcp-go/server" ) // ToolRegistry manages the registration and building of all tool builders @@ -115,11 +113,11 @@ func (r *ToolRegistry) ListMetadata() map[string]ToolMetadata { // BuildAll builds tools for all specified configurations // Returns all successfully built tools and any errors encountered -func (r *ToolRegistry) BuildAll(configs map[string]ToolBuildConfig) ([]server.ServerTool, error) { +func (r *ToolRegistry) BuildAll(configs map[string]ToolBuildConfig) ([]ServerTool, error) { r.mu.RLock() defer r.mu.RUnlock() - var allTools []server.ServerTool + var allTools []ServerTool var errors []error for name, config := range configs { @@ -149,7 +147,7 @@ func (r *ToolRegistry) BuildAll(configs map[string]ToolBuildConfig) ([]server.Se } // BuildSingle builds tools for a single tool builder -func (r *ToolRegistry) BuildSingle(name string, config ToolBuildConfig) ([]server.ServerTool, error) { +func (r *ToolRegistry) BuildSingle(name string, config ToolBuildConfig) ([]ServerTool, error) { r.mu.RLock() builder, exists := r.builders[name] r.mu.RUnlock() @@ -167,7 +165,7 @@ func (r *ToolRegistry) BuildSingle(name string, config ToolBuildConfig) ([]serve // BuildAllWithFeatures builds all relevant tools based on the feature list // Automatically creates configuration for each builder -func (r *ToolRegistry) BuildAllWithFeatures(readOnly bool, features []string) ([]server.ServerTool, error) { +func (r *ToolRegistry) BuildAllWithFeatures(readOnly bool, features []string) ([]ServerTool, error) { r.mu.RLock() builders := make(map[string]ToolBuilder, len(r.builders)) for name, builder := range r.builders { @@ -175,7 +173,7 @@ func (r *ToolRegistry) BuildAllWithFeatures(readOnly bool, features []string) ([ } r.mu.RUnlock() - var allTools []server.ServerTool + var allTools []ServerTool var errors []error for name, builder := range builders { diff --git a/pkg/mcp/builders/registry_test.go b/pkg/mcp/builders/registry_test.go index a4de66b..80d208d 100644 --- a/pkg/mcp/builders/registry_test.go +++ b/pkg/mcp/builders/registry_test.go @@ -19,8 +19,8 @@ import ( "fmt" "testing" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -29,7 +29,7 @@ import ( type MockToolBuilder struct { name string features []string - tools []server.ServerTool + tools []ServerTool err error metadata ToolMetadata } @@ -44,13 +44,13 @@ func NewMockToolBuilder(name string, features []string) *MockToolBuilder { Description: fmt.Sprintf("Mock tool builder for %s", name), Category: "test", }, - tools: []server.ServerTool{ + tools: []ServerTool{ { - Tool: mcp.NewTool(name, - mcp.WithDescription(fmt.Sprintf("Mock tool %s", name)), + Tool: NewTool(name, + WithDescription(fmt.Sprintf("Mock tool %s", name)), ), - Handler: func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return mcp.NewToolResultText(fmt.Sprintf("Mock response from %s", name)), nil + Handler: func(_ context.Context, _ *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + return adapter.NewTextResult(fmt.Sprintf("Mock response from %s", name)), nil }, }, }, @@ -65,7 +65,7 @@ func (m *MockToolBuilder) GetRequiredFeatures() []string { return m.features } -func (m *MockToolBuilder) BuildTools(_ context.Context, _ ToolBuildConfig) ([]server.ServerTool, error) { +func (m *MockToolBuilder) BuildTools(_ context.Context, _ ToolBuildConfig) ([]ServerTool, error) { if m.err != nil { return nil, m.err } diff --git a/pkg/mcp/builders/tool_helpers.go b/pkg/mcp/builders/tool_helpers.go new file mode 100644 index 0000000..a761bbe --- /dev/null +++ b/pkg/mcp/builders/tool_helpers.go @@ -0,0 +1,295 @@ +// Copyright 2025 StreamNative +// +// Licensed 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 builders + +import ( + "github.com/invopop/jsonschema" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// ToolOption is a function that modifies a Tool being built. +// This provides compatibility with the mark3labs builder pattern. +type ToolOption func(*mcpsdk.Tool) + +// NewTool creates a new Tool with the given name and options. +func NewTool(name string, opts ...ToolOption) *mcpsdk.Tool { + tool := &mcpsdk.Tool{ + Name: name, + } + for _, opt := range opts { + opt(tool) + } + return tool +} + +// WithDescription sets the tool description. +func WithDescription(desc string) ToolOption { + return func(t *mcpsdk.Tool) { + t.Description = desc + } +} + +// WithString adds a string property to the input schema. +func WithString(name string, opts ...PropertyOption) ToolOption { + return func(t *mcpsdk.Tool) { + prop := &jsonschema.Schema{ + Type: "string", + } + isRequired := false + for _, opt := range opts { + if opt.required { + isRequired = true + } + if opt.description != "" { + prop.Description = opt.description + } + if len(opt.enumValues) > 0 { + // Convert []string to []any for jsonschema.Schema.Enum + enum := make([]any, len(opt.enumValues)) + for i, v := range opt.enumValues { + enum[i] = v + } + prop.Enum = enum + } + if opt.defaultString != nil { + prop.Default = *opt.defaultString + } + } + addPropertyToTool(t, name, prop, isRequired) + } +} + +// WithNumber adds a number property to the input schema. +func WithNumber(name string, opts ...PropertyOption) ToolOption { + return func(t *mcpsdk.Tool) { + prop := &jsonschema.Schema{ + Type: "number", + } + isRequired := false + for _, opt := range opts { + if opt.required { + isRequired = true + } + if opt.description != "" { + prop.Description = opt.description + } + if opt.defaultNumber != nil { + prop.Default = *opt.defaultNumber + } + } + addPropertyToTool(t, name, prop, isRequired) + } +} + +// WithBoolean adds a boolean property to the input schema. +func WithBoolean(name string, opts ...PropertyOption) ToolOption { + return func(t *mcpsdk.Tool) { + prop := &jsonschema.Schema{ + Type: "boolean", + } + isRequired := false + for _, opt := range opts { + if opt.required { + isRequired = true + } + if opt.description != "" { + prop.Description = opt.description + } + if opt.defaultBool != nil { + prop.Default = *opt.defaultBool + } + } + addPropertyToTool(t, name, prop, isRequired) + } +} + +// WithObject adds an object property to the input schema. +func WithObject(name string, opts ...PropertyOption) ToolOption { + return func(t *mcpsdk.Tool) { + prop := &jsonschema.Schema{ + Type: "object", + } + isRequired := false + for _, opt := range opts { + if opt.required { + isRequired = true + } + if opt.description != "" { + prop.Description = opt.description + } + } + addPropertyToTool(t, name, prop, isRequired) + } +} + +// WithArray adds an array property to the input schema. +func WithArray(name string, opts ...PropertyOption) ToolOption { + return func(t *mcpsdk.Tool) { + prop := &jsonschema.Schema{ + Type: "array", + } + isRequired := false + for _, opt := range opts { + if opt.required { + isRequired = true + } + if opt.description != "" { + prop.Description = opt.description + } + if opt.items != nil { + prop.Items = opt.items + } + } + addPropertyToTool(t, name, prop, isRequired) + } +} + +// Items returns the items schema for array properties. +// This is used to specify the schema of array elements. +// The items parameter can be a *jsonschema.Schema or a map[string]any +// that will be converted to a schema. +func Items(items interface{}) PropertyOption { + var itemsSchema *jsonschema.Schema + switch v := items.(type) { + case *jsonschema.Schema: + itemsSchema = v + case map[string]any: + // Convert map to jsonschema.Schema + itemsSchema = mapToSchema(v) + default: + // Handle map[string]interface{} which is the same as map[string]any + if m, ok := items.(map[string]interface{}); ok { + // Convert each value to any type + converted := make(map[string]any, len(m)) + for k, v := range m { + converted[k] = v + } + itemsSchema = mapToSchema(converted) + } + } + return PropertyOption{items: itemsSchema} +} + +// PropertyOption is an option for modifying a property schema. +type PropertyOption struct { + description string + required bool + items *jsonschema.Schema + enumValues []string + defaultBool *bool + defaultString *string + defaultNumber *float64 +} + +// mapToSchema converts a map[string]any to a jsonschema.Schema. +func mapToSchema(m map[string]any) *jsonschema.Schema { + schema := &jsonschema.Schema{} + if typ, ok := m["type"].(string); ok { + schema.Type = typ + } + if desc, ok := m["description"].(string); ok { + schema.Description = desc + } + return schema +} + +// Description sets the property description. +func Description(desc string) PropertyOption { + return PropertyOption{description: desc} +} + +// Required marks the property as required. +func Required() PropertyOption { + return PropertyOption{required: true} +} + +// addPropertyToTool adds a property to the tool's input schema. +func addPropertyToTool(tool *mcpsdk.Tool, name string, prop *jsonschema.Schema, isRequired bool) { + // Initialize InputSchema if needed + if tool.InputSchema == nil { + props := jsonschema.NewProperties() + required := []string{} + if isRequired { + required = []string{name} + } + tool.InputSchema = &jsonschema.Schema{ + Type: "object", + Properties: props, + Required: required, + } + props.Set(name, prop) + return + } + + // Handle *jsonschema.Schema case + if schema, ok := tool.InputSchema.(*jsonschema.Schema); ok && schema != nil { + if schema.Properties == nil { + schema.Properties = jsonschema.NewProperties() + } + schema.Properties.Set(name, prop) + if isRequired { + if schema.Required == nil { + schema.Required = []string{name} + } else { + schema.Required = append(schema.Required, name) + } + } + } +} + +// Enum adds enum values to a PropertyOption. +// This is used to specify a set of allowed values for a property. +func Enum(values ...string) PropertyOption { + return PropertyOption{enumValues: values} +} + +// DefaultBool adds a default boolean value to a PropertyOption. +// This is used to specify the default value for a boolean property. +func DefaultBool(defaultValue bool) PropertyOption { + return PropertyOption{defaultBool: &defaultValue} +} + +// WithToolAnnotation adds metadata annotations to a Tool. +// This is used to specify title, destructive hints, and other UI-related metadata. +func WithToolAnnotation(annotation ToolAnnotation) ToolOption { + return func(t *mcpsdk.Tool) { + annotations := &mcpsdk.ToolAnnotations{} + if annotation.Title != "" { + annotations.Title = annotation.Title + } + if annotation.DestructiveHint != nil { + annotations.DestructiveHint = annotation.DestructiveHint + } + t.Annotations = annotations + } +} + +// ToolAnnotation represents tool metadata for UI purposes. +type ToolAnnotation struct { + Title string + DestructiveHint *bool +} + +// DefaultString adds a default string value to a PropertyOption. +// This is used to specify the default value for a string property. +func DefaultString(defaultValue string) PropertyOption { + return PropertyOption{defaultString: &defaultValue} +} + +// DefaultNumber adds a default number value to a PropertyOption. +// This is used to specify the default value for a number property. +func DefaultNumber(defaultValue float64) PropertyOption { + return PropertyOption{defaultNumber: &defaultValue} +} diff --git a/pkg/mcp/compat.go b/pkg/mcp/compat.go new file mode 100644 index 0000000..a9db08d --- /dev/null +++ b/pkg/mcp/compat.go @@ -0,0 +1,29 @@ +// Copyright 2025 StreamNative +// +// Licensed 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 mcp + +import ( + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// MCPServer is a type alias for go-sdk's Server. +// This provides backward compatibility with existing tool registration code. +type MCPServer = mcpsdk.Server + +// AddTool is a compatibility wrapper for adding tools to the server. +// This matches the signature used by mark3labs/mcp-go server. +func (s *Server) AddToolCompat(tool interface{}, handler interface{}) { + s.AddTool(tool, handler) +} diff --git a/pkg/mcp/e2e/e2e_test.go b/pkg/mcp/e2e/e2e_test.go new file mode 100644 index 0000000..17627a9 --- /dev/null +++ b/pkg/mcp/e2e/e2e_test.go @@ -0,0 +1,108 @@ +// Copyright 2025 StreamNative +// +// Licensed 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. + +//go:build e2e + +package e2e_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/testutil" + "github.com/stretchr/testify/require" +) + +// setupPulsarE2EServer sets up a Pulsar E2E test server. +// It uses environment variables for configuration or falls back to defaults. +func setupPulsarE2EServer(t *testing.T) *testutil.PulsarE2ETestServer { + adminURL := os.Getenv("PULSAR_ADMIN_URL") + if adminURL == "" { + adminURL = "http://localhost:8080" + } + + serviceURL := os.Getenv("PULSAR_SERVICE_URL") + if serviceURL == "" { + serviceURL = "pulsar://localhost:6650" + } + + server := testutil.NewPulsarE2ETestServer(t, adminURL, serviceURL) + + return server +} + +// TestE2EPulsarConnection verifies that we can connect to Pulsar. +func TestE2EPulsarConnection(t *testing.T) { + t.Parallel() + server := setupPulsarE2EServer(t) + ctx := context.Background() + + // Verify connection by listing tools + response, err := server.Session.ListTools(ctx, &mcp.ListToolsParams{}) + require.NoError(t, err, "failed to list tools") + require.NotNil(t, response) + require.NotEmpty(t, response.Tools, "expected at least one tool") + + // Verify pulsar_admin_topic tool is registered + found := false + for _, tool := range response.Tools { + if tool.Name == "pulsar_admin_topic" { + found = true + break + } + } + require.True(t, found, "pulsar_admin_topic tool not found") +} + +// TestE2EPulsarWaitForReady tests the WaitForReady helper. +func TestE2EPulsarWaitForReady(t *testing.T) { + t.Parallel() + adminURL := os.Getenv("PULSAR_ADMIN_URL") + if adminURL == "" { + adminURL = "http://localhost:8080" + } + + serviceURL := os.Getenv("PULSAR_SERVICE_URL") + if serviceURL == "" { + serviceURL = "pulsar://localhost:6650" + } + + helper, err := testutil.NewPulsarTestHelper(adminURL, serviceURL) + require.NoError(t, err, "failed to create pulsar helper") + t.Cleanup(func() { helper.Close() }) + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + err = helper.WaitForReady(ctx) + require.NoError(t, err, "pulsar not ready") +} + +// cleanupTestTopic is a helper to cleanup a test topic. +func cleanupTestTopic(t *testing.T, helper *testutil.PulsarTestHelper, topic string) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := helper.CleanupTopic(ctx, topic); err != nil { + t.Logf("Warning: failed to cleanup topic %s: %v", topic, err) + } +} + +// generateTestTopicName generates a unique test topic name. +func generateTestTopicName() string { + return testutil.GenerateTestTopicName("test") +} diff --git a/pkg/mcp/e2e/pulsar_topic_test.go b/pkg/mcp/e2e/pulsar_topic_test.go new file mode 100644 index 0000000..f9c0403 --- /dev/null +++ b/pkg/mcp/e2e/pulsar_topic_test.go @@ -0,0 +1,266 @@ +// Copyright 2025 StreamNative +// +// Licensed 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. + +//go:build e2e + +package e2e_test + +import ( + "context" + "encoding/json" + "testing" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/stretchr/testify/require" +) + +// TestPulsarTopicList tests listing topics in a namespace. +func TestPulsarTopicList(t *testing.T) { + t.Parallel() + + server := setupPulsarE2EServer(t) + ctx := context.Background() + + // List topics in public/default namespace + response, err := server.Session.CallTool(ctx, &mcp.CallToolParams{ + Name: "pulsar_admin_topic", + Arguments: map[string]any{ + "resource": "topics", + "operation": "list", + "namespace": "public/default", + }, + }) + + require.NoError(t, err, "failed to call pulsar_admin_topic tool") + require.NotNil(t, response) + require.False(t, response.IsError, "expected success, got error: %s", getErrorText(response)) + require.Len(t, response.Content, 1, "expected one content item") + + textContent, ok := response.Content[0].(*mcp.TextContent) + require.True(t, ok, "expected TextContent") + + // Verify response is a JSON array + var topics []string + err = json.Unmarshal([]byte(textContent.Text), &topics) + require.NoError(t, err, "expected JSON array response") +} + +// TestPulsarTopicCreateGetDelete tests creating, getting, and deleting a topic. +func TestPulsarTopicCreateGetDelete(t *testing.T) { + t.Parallel() + + server := setupPulsarE2EServer(t) + ctx := context.Background() + + testTopic := generateTestTopicName() + t.Logf("Using test topic: %s", testTopic) + + // Cleanup on test failure + t.Cleanup(func() { + cleanupTestTopic(t, server.PulsarHelper, testTopic) + }) + + // 1. Create topic + t.Run("Create", func(t *testing.T) { + createResp, err := server.Session.CallTool(ctx, &mcp.CallToolParams{ + Name: "pulsar_admin_topic", + Arguments: map[string]any{ + "resource": "topic", + "operation": "create", + "topic": testTopic, + "partitions": 0, + }, + }) + + require.NoError(t, err, "failed to call create operation") + require.NotNil(t, createResp) + require.False(t, createResp.IsError, "expected success, got error: %s", getErrorText(createResp)) + require.Len(t, createResp.Content, 1, "expected one content item") + + textContent, ok := createResp.Content[0].(*mcp.TextContent) + require.True(t, ok, "expected TextContent") + + var result map[string]string + err = json.Unmarshal([]byte(textContent.Text), &result) + require.NoError(t, err) + require.Equal(t, "created", result["status"]) + require.Equal(t, testTopic, result["topic"]) + }) + + // 2. Get topic metadata + t.Run("Get", func(t *testing.T) { + getResp, err := server.Session.CallTool(ctx, &mcp.CallToolParams{ + Name: "pulsar_admin_topic", + Arguments: map[string]any{ + "resource": "topic", + "operation": "get", + "topic": testTopic, + }, + }) + + require.NoError(t, err, "failed to call get operation") + require.NotNil(t, getResp) + require.False(t, getResp.IsError, "expected success, got error: %s", getErrorText(getResp)) + require.Len(t, getResp.Content, 1, "expected one content item") + + textContent, ok := getResp.Content[0].(*mcp.TextContent) + require.True(t, ok, "expected TextContent") + + var metadata map[string]interface{} + err = json.Unmarshal([]byte(textContent.Text), &metadata) + require.NoError(t, err) + require.Equal(t, testTopic, metadata["name"]) + }) + + // 3. Delete topic + t.Run("Delete", func(t *testing.T) { + deleteResp, err := server.Session.CallTool(ctx, &mcp.CallToolParams{ + Name: "pulsar_admin_topic", + Arguments: map[string]any{ + "resource": "topic", + "operation": "delete", + "topic": testTopic, + }, + }) + + require.NoError(t, err, "failed to call delete operation") + require.NotNil(t, deleteResp) + require.False(t, deleteResp.IsError, "expected success, got error: %s", getErrorText(deleteResp)) + require.Len(t, deleteResp.Content, 1, "expected one content item") + + textContent, ok := deleteResp.Content[0].(*mcp.TextContent) + require.True(t, ok, "expected TextContent") + + var result map[string]string + err = json.Unmarshal([]byte(textContent.Text), &result) + require.NoError(t, err) + require.Equal(t, "deleted", result["status"]) + require.Equal(t, testTopic, result["topic"]) + }) +} + +// TestPulsarTopicCreateWithPartitions tests creating a partitioned topic. +func TestPulsarTopicCreateWithPartitions(t *testing.T) { + t.Parallel() + + server := setupPulsarE2EServer(t) + ctx := context.Background() + + testTopic := generateTestTopicName() + t.Logf("Using test topic: %s", testTopic) + + t.Cleanup(func() { + cleanupTestTopic(t, server.PulsarHelper, testTopic) + }) + + response, err := server.Session.CallTool(ctx, &mcp.CallToolParams{ + Name: "pulsar_admin_topic", + Arguments: map[string]any{ + "resource": "topic", + "operation": "create", + "topic": testTopic, + "partitions": 3, + }, + }) + + require.NoError(t, err, "failed to call create operation") + require.NotNil(t, response) + require.False(t, response.IsError, "expected success, got error: %s", getErrorText(response)) + require.Len(t, response.Content, 1, "expected one content item") + + // Verify the topic was created with partitions + getResp, err := server.Session.CallTool(ctx, &mcp.CallToolParams{ + Name: "pulsar_admin_topic", + Arguments: map[string]any{ + "resource": "topic", + "operation": "get", + "topic": testTopic, + }, + }) + + require.NoError(t, err, "failed to call get operation") + require.False(t, getResp.IsError, "expected success, got error: %s", getErrorText(getResp)) + + textContent, ok := getResp.Content[0].(*mcp.TextContent) + require.True(t, ok, "expected TextContent") + + var metadata map[string]interface{} + err = json.Unmarshal([]byte(textContent.Text), &metadata) + require.NoError(t, err) + require.Equal(t, testTopic, metadata["name"]) + + // Partitions field should be present and > 0 + partitions, ok := metadata["partitions"] + require.True(t, ok, "expected partitions field") + require.Equal(t, float64(3), partitions) +} + +// TestPulsarTopicErrorCases tests error handling for invalid requests. +func TestPulsarTopicErrorCases(t *testing.T) { + t.Parallel() + + server := setupPulsarE2EServer(t) + ctx := context.Background() + + t.Run("MissingRequiredParameter", func(t *testing.T) { + response, err := server.Session.CallTool(ctx, &mcp.CallToolParams{ + Name: "pulsar_admin_topic", + Arguments: map[string]any{ + // Missing "resource" and "operation" + }, + }) + + require.NoError(t, err) + require.True(t, response.IsError, "expected error for missing parameters") + }) + + t.Run("InvalidOperation", func(t *testing.T) { + response, err := server.Session.CallTool(ctx, &mcp.CallToolParams{ + Name: "pulsar_admin_topic", + Arguments: map[string]any{ + "resource": "topic", + "operation": "invalid_op", + }, + }) + + require.NoError(t, err) + require.True(t, response.IsError, "expected error for invalid operation") + require.Contains(t, getErrorText(response), "unsupported operation") + }) + + t.Run("CreateMissingTopic", func(t *testing.T) { + response, err := server.Session.CallTool(ctx, &mcp.CallToolParams{ + Name: "pulsar_admin_topic", + Arguments: map[string]any{ + "resource": "topic", + "operation": "create", + // Missing "topic" + }, + }) + + require.NoError(t, err) + require.True(t, response.IsError, "expected error for missing topic") + require.Contains(t, getErrorText(response), "topic is required") + }) +} + +// getErrorText extracts error text from a CallToolResult. +func getErrorText(response *mcp.CallToolResult) string { + if len(response.Content) > 0 { + if textContent, ok := response.Content[0].(*mcp.TextContent); ok { + return textContent.Text + } + } + return "" +} diff --git a/pkg/mcp/internal/adapter/handler.go b/pkg/mcp/internal/adapter/handler.go new file mode 100644 index 0000000..4d0d8c9 --- /dev/null +++ b/pkg/mcp/internal/adapter/handler.go @@ -0,0 +1,350 @@ +// Copyright 2025 StreamNative +// +// Licensed 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 adapter + +import ( + "context" + "encoding/json" + "fmt" + + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// ToolHandlerFuncV1 represents the mark3labs/mcp-go handler signature. +// Existing handlers use this signature: +// func(ctx context.Context, request CallToolRequest) (*CallToolResult, error) +type ToolHandlerFuncV1 interface{} + +// AdaptHandlerV1ToV2 adapts a mark3labs-style handler to go-sdk's ToolHandler. +// +// The key differences: +// - mark3labs: func(ctx, request) -> (result, error) +// - go-sdk: func(ctx, request) -> (result, error) +// +// Both have similar signatures, but the types are from different packages. +// This adapter handles the type conversion. +func AdaptHandlerV1ToV2(handler ToolHandlerFuncV1) mcpsdk.ToolHandler { + // Try to assert as go-sdk handler first + if h, ok := handler.(func(context.Context, *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error)); ok { + return h + } + // If not a go-sdk handler, return an error handler + // The actual migration will happen when we update tool builders + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{ + Text: "handler needs migration to go-sdk signature", + }, + }, + IsError: true, + }, nil + } +} + +// NewTextResult creates a text result for go-sdk. +func NewTextResult(text string) *mcpsdk.CallToolResult { + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{ + Text: text, + }, + }, + } +} + +// NewErrorResult creates an error result for go-sdk. +func NewErrorResult(format string, args ...any) *mcpsdk.CallToolResult { + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{ + Text: fmt.Sprintf(format, args...), + }, + }, + IsError: true, + } +} + +// AdaptToolDefV1ToV2 converts a mark3labs Tool definition to go-sdk Tool. +// This handles the conversion of tool metadata and input schema. +func AdaptToolDefV1ToV2(toolV1 interface{}) *mcpsdk.Tool { + // During migration, we'll need to convert tool definitions + // For now, tool builders will need to be updated to use go-sdk types + return nil +} + +// RequireString extracts a required string argument from the request. +func RequireString(request *mcpsdk.CallToolRequest, name string) (string, error) { + args, err := GetArgumentsMap(request) + if err != nil { + return "", fmt.Errorf("failed to parse arguments: %w", err) + } + val, ok := args[name] + if !ok { + return "", fmt.Errorf("missing required parameter: %s", name) + } + str, ok := val.(string) + if !ok { + return "", fmt.Errorf("parameter %s is not of type string", name) + } + return str, nil +} + +// RequireOptionalString extracts an optional string argument from the request. +func RequireOptionalString(request *mcpsdk.CallToolRequest, name string) (string, bool, error) { + args, err := GetArgumentsMap(request) + if err != nil { + return "", false, fmt.Errorf("failed to parse arguments: %w", err) + } + val, ok := args[name] + if !ok { + return "", false, nil + } + str, ok := val.(string) + if !ok { + return "", false, fmt.Errorf("parameter %s is not of type string", name) + } + return str, true, nil +} + +// GetArgumentsMap extracts all arguments as a map from the request. +func GetArgumentsMap(request *mcpsdk.CallToolRequest) (map[string]any, error) { + if request.Params.Arguments == nil || len(request.Params.Arguments) == 0 { + return make(map[string]any), nil + } + var args map[string]any + if err := json.Unmarshal(request.Params.Arguments, &args); err != nil { + return nil, fmt.Errorf("failed to unmarshal arguments: %w", err) + } + return args, nil +} + +// GetString extracts an optional string argument from the request. +func GetString(request *mcpsdk.CallToolRequest, name string, defaultValue string) string { + args, err := GetArgumentsMap(request) + if err != nil { + return defaultValue + } + val, ok := args[name] + if !ok { + return defaultValue + } + str, ok := val.(string) + if !ok { + return defaultValue + } + return str +} + +// GetFloat extracts an optional float argument from the request. +func GetFloat(request *mcpsdk.CallToolRequest, name string, defaultValue float64) float64 { + args, err := GetArgumentsMap(request) + if err != nil { + return defaultValue + } + val, ok := args[name] + if !ok { + return defaultValue + } + switch v := val.(type) { + case float64: + return v + case float32: + return float64(v) + case int: + return float64(v) + case int64: + return float64(v) + default: + return defaultValue + } +} + +// GetInt extracts an optional int argument from the request. +func GetInt(request *mcpsdk.CallToolRequest, name string, defaultValue int) int { + args, err := GetArgumentsMap(request) + if err != nil { + return defaultValue + } + val, ok := args[name] + if !ok { + return defaultValue + } + switch v := val.(type) { + case int: + return v + case int64: + return int(v) + case float64: + return int(v) + default: + return defaultValue + } +} + +// GetBool extracts an optional bool argument from the request. +func GetBool(request *mcpsdk.CallToolRequest, name string, defaultValue bool) bool { + args, err := GetArgumentsMap(request) + if err != nil { + return defaultValue + } + val, ok := args[name] + if !ok { + return defaultValue + } + switch v := val.(type) { + case bool: + return v + case string: + return v == "true" || v == "1" + default: + return defaultValue + } +} + +// GetStringSlice extracts an optional string slice argument from the request. +func GetStringSlice(request *mcpsdk.CallToolRequest, name string, defaultValue []string) []string { + args, err := GetArgumentsMap(request) + if err != nil { + return defaultValue + } + val, ok := args[name] + if !ok { + return defaultValue + } + // Try to convert to []string + switch v := val.(type) { + case []string: + return v + case []interface{}: + result := make([]string, 0, len(v)) + for _, item := range v { + if str, ok := item.(string); ok { + result = append(result, str) + } + } + return result + default: + return defaultValue + } +} + +// RequireInt extracts a required int argument from the request. +func RequireInt(request *mcpsdk.CallToolRequest, name string) (int, error) { + args, err := GetArgumentsMap(request) + if err != nil { + return 0, fmt.Errorf("failed to parse arguments: %w", err) + } + val, ok := args[name] + if !ok { + return 0, fmt.Errorf("missing required parameter: %s", name) + } + switch v := val.(type) { + case int: + return v, nil + case int64: + return int(v), nil + case float64: + return int(v), nil + default: + return 0, fmt.Errorf("parameter %s is not of type int", name) + } +} + +// RequireFloat extracts a required float argument from the request. +func RequireFloat(request *mcpsdk.CallToolRequest, name string) (float64, error) { + args, err := GetArgumentsMap(request) + if err != nil { + return 0, fmt.Errorf("failed to parse arguments: %w", err) + } + val, ok := args[name] + if !ok { + return 0, fmt.Errorf("missing required parameter: %s", name) + } + switch v := val.(type) { + case float64: + return v, nil + case float32: + return float64(v), nil + case int: + return float64(v), nil + case int64: + return float64(v), nil + default: + return 0, fmt.Errorf("parameter %s is not of type float", name) + } +} + +// RequireStringSlice extracts a required string slice argument from the request. +func RequireStringSlice(request *mcpsdk.CallToolRequest, name string) ([]string, error) { + args, err := GetArgumentsMap(request) + if err != nil { + return nil, fmt.Errorf("failed to parse arguments: %w", err) + } + val, ok := args[name] + if !ok { + return nil, fmt.Errorf("missing required parameter: %s", name) + } + switch v := val.(type) { + case []string: + return v, nil + case []interface{}: + result := make([]string, 0, len(v)) + for _, item := range v { + if str, ok := item.(string); ok { + result = append(result, str) + } else { + return nil, fmt.Errorf("parameter %s contains non-string element", name) + } + } + return result, nil + default: + return nil, fmt.Errorf("parameter %s is not of type []string", name) + } +} + +// RequireObject extracts a required object argument from the request. +func RequireObject(request *mcpsdk.CallToolRequest, name string) (map[string]any, error) { + args, err := GetArgumentsMap(request) + if err != nil { + return nil, fmt.Errorf("failed to parse arguments: %w", err) + } + val, ok := args[name] + if !ok { + return nil, fmt.Errorf("missing required parameter: %s", name) + } + obj, ok := val.(map[string]any) + if !ok { + return nil, fmt.Errorf("parameter %s is not of type object", name) + } + return obj, nil +} + +// GetObject extracts an optional object argument from the request. +func GetObject(request *mcpsdk.CallToolRequest, name string) map[string]any { + args, err := GetArgumentsMap(request) + if err != nil { + return nil + } + val, ok := args[name] + if !ok { + return nil + } + obj, ok := val.(map[string]any) + if !ok { + return nil + } + return obj +} diff --git a/pkg/mcp/internal/testutil/pulsar.go b/pkg/mcp/internal/testutil/pulsar.go new file mode 100644 index 0000000..e99d97f --- /dev/null +++ b/pkg/mcp/internal/testutil/pulsar.go @@ -0,0 +1,221 @@ +// Copyright 2025 StreamNative +// +// Licensed 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. + +//go:build e2e + +package testutil + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/apache/pulsar-client-go/pulsaradmin" + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/admin/config" + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" +) + +// PulsarTestHelper provides helper functions for Pulsar E2E testing. +type PulsarTestHelper struct { + adminClient pulsaradmin.Client + adminURL string + serviceURL string + httpClient *http.Client +} + +// NewPulsarTestHelper creates a new PulsarTestHelper. +func NewPulsarTestHelper(adminURL, serviceURL string) (*PulsarTestHelper, error) { + if adminURL == "" { + adminURL = "http://localhost:8080" + } + if serviceURL == "" { + serviceURL = "pulsar://localhost:6650" + } + + cfg := &config.Config{} + cfg.WebServiceURL = adminURL + + client, err := pulsaradmin.NewClient(cfg) + if err != nil { + return nil, fmt.Errorf("failed to create pulsar admin client: %w", err) + } + + return &PulsarTestHelper{ + adminClient: client, + adminURL: adminURL, + serviceURL: serviceURL, + httpClient: &http.Client{ + Timeout: 30 * time.Second, + }, + }, nil +} + +// Close closes the Pulsar admin client's underlying resources. +func (h *PulsarTestHelper) Close() { + // pulsaradmin.Client interface doesn't have Close method + // The underlying HTTP client will be garbage collected +} + +// WaitForReady waits for Pulsar to be ready by checking the admin API. +func (h *PulsarTestHelper) WaitForReady(ctx context.Context) error { + ctx, cancel := context.WithTimeout(ctx, 60*time.Second) + defer cancel() + + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + url := fmt.Sprintf("%s/admin/v2/clusters", h.adminURL) + + for { + select { + case <-ctx.Done(): + return fmt.Errorf("timeout waiting for pulsar to be ready") + case <-ticker.C: + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + continue + } + + resp, err := h.httpClient.Do(req) + if err == nil && resp.StatusCode == http.StatusOK { + _ = resp.Body.Close() + return nil + } + if resp != nil { + _ = resp.Body.Close() + } + } + } +} + +// EnsureNamespace ensures the specified namespace exists. +func (h *PulsarTestHelper) EnsureNamespace(ctx context.Context, tenant, namespace string) error { + fullNamespace := tenant + "/" + namespace + + // Check if namespace exists + namespaces, err := h.adminClient.Namespaces().GetNamespaces(tenant) + if err == nil { + for _, ns := range namespaces { + if ns == fullNamespace { + return nil // Already exists + } + } + } + + // Create namespace + err = h.adminClient.Namespaces().CreateNamespace(fullNamespace) + if err != nil { + return fmt.Errorf("failed to create namespace %s: %w", fullNamespace, err) + } + + return nil +} + +// CreateTopic creates a Pulsar topic with the specified name and partition count. +func (h *PulsarTestHelper) CreateTopic(ctx context.Context, topic string, partitions int) error { + topicName, err := utils.GetTopicName(topic) + if err != nil { + return fmt.Errorf("invalid topic name %s: %w", topic, err) + } + + // Use dereference as done in existing code (see pkg/mcp/builders/pulsar/topic.go) + if err := h.adminClient.Topics().Create(*topicName, partitions); err != nil { + return fmt.Errorf("failed to create topic %s: %w", topic, err) + } + return nil +} + +// TopicExists checks if a topic exists. +func (h *PulsarTestHelper) TopicExists(ctx context.Context, topic string) (bool, error) { + topicName, err := utils.GetTopicName(topic) + if err != nil { + return false, fmt.Errorf("invalid topic name %s: %w", topic, err) + } + + _, err = h.adminClient.Topics().GetMetadata(*topicName) + if err != nil { + return false, fmt.Errorf("failed to get metadata for topic %s: %w", topic, err) + } + return true, nil +} + +// DeleteTopic deletes a Pulsar topic. +func (h *PulsarTestHelper) DeleteTopic(ctx context.Context, topic string) error { + topicName, err := utils.GetTopicName(topic) + if err != nil { + return fmt.Errorf("invalid topic name %s: %w", topic, err) + } + + // Delete with force=true, treat as non-partitioned (works for both) + if err := h.adminClient.Topics().Delete(*topicName, true, true); err != nil { + // Try as partitioned + if err2 := h.adminClient.Topics().Delete(*topicName, true, false); err2 != nil { + return fmt.Errorf("failed to delete topic %s: %w", topic, err) + } + } + return nil +} + +// CleanupTopic cleans up a topic if it exists. +func (h *PulsarTestHelper) CleanupTopic(ctx context.Context, topic string) error { + exists, err := h.TopicExists(ctx, topic) + if err != nil { + return err + } + if exists { + return h.DeleteTopic(ctx, topic) + } + return nil +} + +// ListTopics lists topics in a namespace. +func (h *PulsarTestHelper) ListTopics(namespace string) ([]string, error) { + nsName, err := utils.GetNamespaceName(namespace) + if err != nil { + return nil, fmt.Errorf("invalid namespace name %s: %w", namespace, err) + } + + topics, _, err := h.adminClient.Topics().List(*nsName) + if err != nil { + return nil, fmt.Errorf("failed to list topics: %w", err) + } + return topics, nil +} + +// GetTopicMetadata gets metadata for a topic. +func (h *PulsarTestHelper) GetTopicMetadata(topic string) (map[string]interface{}, error) { + topicName, err := utils.GetTopicName(topic) + if err != nil { + return nil, fmt.Errorf("invalid topic name %s: %w", topic, err) + } + + metadata, err := h.adminClient.Topics().GetMetadata(*topicName) + if err != nil { + return nil, fmt.Errorf("failed to get topic metadata: %w", err) + } + + // PartitionedTopicMetadata only has Partitions field + result := map[string]interface{}{ + "name": topic, + "partitions": metadata.Partitions, + } + return result, nil +} + +// GenerateTestTopicName generates a unique test topic name. +func GenerateTestTopicName(prefix string) string { + timestamp := time.Now().UnixNano() + return fmt.Sprintf("persistent://public/default/%s-%d", prefix, timestamp) +} diff --git a/pkg/mcp/internal/testutil/server.go b/pkg/mcp/internal/testutil/server.go new file mode 100644 index 0000000..3eff017 --- /dev/null +++ b/pkg/mcp/internal/testutil/server.go @@ -0,0 +1,238 @@ +// Copyright 2025 StreamNative +// +// Licensed 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. + +//go:build e2e + +package testutil + +import ( + "context" + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/stretchr/testify/require" +) + +// PulsarE2ETestServer encapsulates the E2E test components. +type PulsarE2ETestServer struct { + Server *mcp.Server + Session *mcp.ClientSession + ServerCtx context.Context + ClientCtx context.Context + ServerCancel context.CancelFunc + ClientCancel context.CancelFunc + PulsarHelper *PulsarTestHelper + TestNamespace string +} + +// NewPulsarE2ETestServer creates a new E2E test server with in-memory transport. +func NewPulsarE2ETestServer(t testing.TB, adminURL, serviceURL string) *PulsarE2ETestServer { + // Create Pulsar helper + pulsarHelper, err := NewPulsarTestHelper(adminURL, serviceURL) + require.NoError(t, err, "failed to create pulsar helper") + + // Wait for Pulsar to be ready + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + require.NoError(t, pulsarHelper.WaitForReady(ctx), "pulsar not ready") + + // Ensure test namespace exists + require.NoError(t, pulsarHelper.EnsureNamespace(context.Background(), "public", "default")) + + // Create in-memory transports + serverTransport, clientTransport := mcp.NewInMemoryTransports() + + // Create go-sdk server + server := mcp.NewServer(&mcp.Implementation{ + Name: "test-server", + Version: "0.0.1", + }, &mcp.ServerOptions{}) + + // Register test tools + registerPulsarTopicTools(t, server, pulsarHelper) + + // Start server + serverCtx, serverCancel := context.WithCancel(context.Background()) + go func() { + _ = server.Run(serverCtx, serverTransport) + }() + + // Create client + clientCtx, clientCancel := context.WithCancel(context.Background()) + client := mcp.NewClient(&mcp.Implementation{ + Name: "test-client", + Version: "0.0.1", + }, nil) + + session, err := client.Connect(clientCtx, clientTransport, nil) + require.NoError(t, err, "failed to connect client") + + t.Cleanup(func() { + _ = session.Close() + serverCancel() + clientCancel() + pulsarHelper.Close() + }) + + return &PulsarE2ETestServer{ + Server: server, + Session: session, + ServerCtx: serverCtx, + ClientCtx: clientCtx, + ServerCancel: serverCancel, + ClientCancel: clientCancel, + PulsarHelper: pulsarHelper, + TestNamespace: "public/default", + } +} + +// registerPulsarTopicTools registers the pulsar_admin_topic tool for E2E testing. +// This is a simplified version that directly calls Pulsar admin API. +func registerPulsarTopicTools(t testing.TB, server *mcp.Server, helper *PulsarTestHelper) { + tool := &mcp.Tool{ + Name: "pulsar_admin_topic", + Description: "Manage Pulsar topics. Supports list, get, create, and delete operations.", + InputSchema: json.RawMessage(`{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "resource": { + "type": "string", + "description": "Resource to operate on: 'topic' or 'topics'" + }, + "operation": { + "type": "string", + "description": "Operation: 'list', 'get', 'create', 'delete'" + }, + "topic": { + "type": "string", + "description": "Fully qualified topic name (e.g., persistent://public/default/test)" + }, + "namespace": { + "type": "string", + "description": "Namespace name (e.g., public/default)" + }, + "partitions": { + "type": "integer", + "description": "Number of partitions (0 for non-partitioned)" + } + }, + "required": ["resource", "operation"] + }`), + } + + // Handler using go-sdk signature: func(context.Context, *CallToolRequest) (*CallToolResult, error) + // req.Params.Arguments is json.RawMessage that needs unmarshaling + handler := func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { + var args map[string]any + _ = json.Unmarshal(req.Params.Arguments, &args) + resource, _ := args["resource"].(string) + operation, _ := args["operation"].(string) + + switch operation { + case "list": + if resource != "topics" { + return newToolResultError("list operation requires 'topics' resource"), nil + } + namespace, _ := args["namespace"].(string) + if namespace == "" { + namespace = "public/default" + } + + topics, err := helper.ListTopics(namespace) + if err != nil { + return newToolResultError(fmt.Sprintf("failed to list topics: %v", err)), nil + } + + result, _ := json.Marshal(topics) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: string(result)}, + }, + }, nil + + case "get": + topic, _ := args["topic"].(string) + if topic == "" { + return newToolResultError("topic is required for get operation"), nil + } + + metadata, err := helper.GetTopicMetadata(topic) + if err != nil { + return newToolResultError(fmt.Sprintf("failed to get topic: %v", err)), nil + } + + result, _ := json.Marshal(metadata) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: string(result)}, + }, + }, nil + + case "create": + topic, _ := args["topic"].(string) + if topic == "" { + return newToolResultError("topic is required for create operation"), nil + } + partitionsVal, _ := args["partitions"].(float64) + partitions := int(partitionsVal) + + if err := helper.CreateTopic(ctx, topic, partitions); err != nil { + return newToolResultError(fmt.Sprintf("failed to create topic: %v", err)), nil + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: fmt.Sprintf(`{"status": "created", "topic": "%s"}`, topic)}, + }, + }, nil + + case "delete": + topic, _ := args["topic"].(string) + if topic == "" { + return newToolResultError("topic is required for delete operation"), nil + } + + if err := helper.DeleteTopic(ctx, topic); err != nil { + return newToolResultError(fmt.Sprintf("failed to delete topic: %v", err)), nil + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: fmt.Sprintf(`{"status": "deleted", "topic": "%s"}`, topic)}, + }, + }, nil + + default: + return newToolResultError(fmt.Sprintf("unsupported operation: %s", operation)), nil + } + } + + server.AddTool(tool, handler) +} + +// newToolResultError creates an error tool result. +func newToolResultError(message string) *mcp.CallToolResult { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{ + Text: message, + }, + }, + IsError: true, + } +} diff --git a/pkg/mcp/kafka_admin_connect_tools.go b/pkg/mcp/kafka_admin_connect_tools.go index e85fa10..eff7b4d 100644 --- a/pkg/mcp/kafka_admin_connect_tools.go +++ b/pkg/mcp/kafka_admin_connect_tools.go @@ -17,12 +17,11 @@ package mcp import ( "context" - "github.com/mark3labs/mcp-go/server" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" kafkabuilders "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders/kafka" ) -func KafkaAdminAddKafkaConnectTools(s *server.MCPServer, readOnly bool, features []string) { +func KafkaAdminAddKafkaConnectTools(s *MCPServer, readOnly bool, features []string) { // Use the new builder pattern builder := kafkabuilders.NewKafkaConnectToolBuilder() config := builders.ToolBuildConfig{ diff --git a/pkg/mcp/kafka_admin_groups_tools.go b/pkg/mcp/kafka_admin_groups_tools.go index 007622a..b5fdc45 100644 --- a/pkg/mcp/kafka_admin_groups_tools.go +++ b/pkg/mcp/kafka_admin_groups_tools.go @@ -17,12 +17,11 @@ package mcp import ( "context" - "github.com/mark3labs/mcp-go/server" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" kafkabuilders "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders/kafka" ) -func KafkaAdminAddGroupsTools(s *server.MCPServer, readOnly bool, features []string) { +func KafkaAdminAddGroupsTools(s *MCPServer, readOnly bool, features []string) { // Use the new builder pattern builder := kafkabuilders.NewKafkaGroupsToolBuilder() config := builders.ToolBuildConfig{ diff --git a/pkg/mcp/kafka_admin_partitions_tools.go b/pkg/mcp/kafka_admin_partitions_tools.go index 3010e81..1db76c3 100644 --- a/pkg/mcp/kafka_admin_partitions_tools.go +++ b/pkg/mcp/kafka_admin_partitions_tools.go @@ -17,12 +17,11 @@ package mcp import ( "context" - "github.com/mark3labs/mcp-go/server" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" kafkabuilders "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders/kafka" ) -func KafkaAdminAddPartitionsTools(s *server.MCPServer, readOnly bool, features []string) { +func KafkaAdminAddPartitionsTools(s *MCPServer, readOnly bool, features []string) { // Use the new builder pattern builder := kafkabuilders.NewKafkaPartitionsToolBuilder() config := builders.ToolBuildConfig{ diff --git a/pkg/mcp/kafka_admin_sr_tools.go b/pkg/mcp/kafka_admin_sr_tools.go index d6590cf..bf00f6e 100644 --- a/pkg/mcp/kafka_admin_sr_tools.go +++ b/pkg/mcp/kafka_admin_sr_tools.go @@ -17,12 +17,11 @@ package mcp import ( "context" - "github.com/mark3labs/mcp-go/server" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" kafkabuilders "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders/kafka" ) -func KafkaAdminAddSchemaRegistryTools(s *server.MCPServer, readOnly bool, features []string) { +func KafkaAdminAddSchemaRegistryTools(s *MCPServer, readOnly bool, features []string) { // Use the new builder pattern builder := kafkabuilders.NewKafkaSchemaRegistryToolBuilder() config := builders.ToolBuildConfig{ diff --git a/pkg/mcp/kafka_admin_topics_tools.go b/pkg/mcp/kafka_admin_topics_tools.go index 9075245..73a06ec 100644 --- a/pkg/mcp/kafka_admin_topics_tools.go +++ b/pkg/mcp/kafka_admin_topics_tools.go @@ -17,12 +17,11 @@ package mcp import ( "context" - "github.com/mark3labs/mcp-go/server" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" kafkabuilders "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders/kafka" ) -func KafkaAdminAddTopicTools(s *server.MCPServer, readOnly bool, features []string) { +func KafkaAdminAddTopicTools(s *MCPServer, readOnly bool, features []string) { // Use the new builder pattern builder := kafkabuilders.NewKafkaTopicsToolBuilder() config := builders.ToolBuildConfig{ diff --git a/pkg/mcp/kafka_client_consume_tools.go b/pkg/mcp/kafka_client_consume_tools.go index d94def2..1b18029 100644 --- a/pkg/mcp/kafka_client_consume_tools.go +++ b/pkg/mcp/kafka_client_consume_tools.go @@ -17,14 +17,13 @@ package mcp import ( "context" - "github.com/mark3labs/mcp-go/server" "github.com/sirupsen/logrus" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" kafkabuilders "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders/kafka" ) // KafkaClientAddConsumeTools adds Kafka client consume tools to the MCP server -func KafkaClientAddConsumeTools(s *server.MCPServer, _ bool, logrusLogger *logrus.Logger, features []string) { +func KafkaClientAddConsumeTools(s *MCPServer, _ bool, logrusLogger *logrus.Logger, features []string) { // Use the new builder pattern builder := kafkabuilders.NewKafkaConsumeToolBuilder() config := builders.ToolBuildConfig{ diff --git a/pkg/mcp/kafka_client_produce_tools.go b/pkg/mcp/kafka_client_produce_tools.go index e75da6a..b3cee13 100644 --- a/pkg/mcp/kafka_client_produce_tools.go +++ b/pkg/mcp/kafka_client_produce_tools.go @@ -17,13 +17,12 @@ package mcp import ( "context" - "github.com/mark3labs/mcp-go/server" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" kafkabuilders "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders/kafka" ) // KafkaClientAddProduceTools adds Kafka client produce tools to the MCP server -func KafkaClientAddProduceTools(s *server.MCPServer, readOnly bool, features []string) { +func KafkaClientAddProduceTools(s *MCPServer, readOnly bool, features []string) { // Use the new builder pattern builder := kafkabuilders.NewKafkaProduceToolBuilder() config := builders.ToolBuildConfig{ diff --git a/pkg/mcp/pftools/invocation.go b/pkg/mcp/pftools/invocation.go index 24c3c37..430e91c 100644 --- a/pkg/mcp/pftools/invocation.go +++ b/pkg/mcp/pftools/invocation.go @@ -25,7 +25,7 @@ import ( "github.com/apache/pulsar-client-go/pulsar" "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/streamnative-mcp-server/pkg/schema" ) @@ -54,15 +54,29 @@ func NewFunctionInvoker(manager *PulsarFunctionManager) *FunctionInvoker { } // InvokeFunctionAndWait sends a message to the function and waits for the result -func (fi *FunctionInvoker) InvokeFunctionAndWait(ctx context.Context, fnTool *FunctionTool, params map[string]interface{}) (*mcp.CallToolResult, error) { +func (fi *FunctionInvoker) InvokeFunctionAndWait(ctx context.Context, fnTool *FunctionTool, params map[string]interface{}) (*mcpsdk.CallToolResult, error) { schemaConverter, err := schema.ConverterFactory(fnTool.OutputSchema.Type) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get schema converter: %v", err)), nil + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{ + Text: fmt.Sprintf("Failed to get schema converter: %v", err), + }, + }, + IsError: true, + }, nil } payload, err := schemaConverter.SerializeMCPRequestToPulsarPayload(params, fnTool.OutputSchema.PulsarSchemaInfo) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to serialize payload: %v", err)), nil + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{ + Text: fmt.Sprintf("Failed to serialize payload: %v", err), + }, + }, + IsError: true, + }, nil } // Create a result channel for this request @@ -71,7 +85,14 @@ func (fi *FunctionInvoker) InvokeFunctionAndWait(ctx context.Context, fnTool *Fu // Send message to input topic msgID, err := fi.sendMessage(ctx, fnTool.InputTopic, payload) if err != nil || msgID == "" { - return mcp.NewToolResultError(fmt.Sprintf("Failed to send message: %v", err)), nil + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{ + Text: fmt.Sprintf("Failed to send message: %v", err), + }, + }, + IsError: true, + }, nil } fi.registerResultChannel(msgID, resultChan) @@ -80,19 +101,46 @@ func (fi *FunctionInvoker) InvokeFunctionAndWait(ctx context.Context, fnTool *Fu // Set up consumer for output topic err = fi.setupConsumer(ctx, fnTool.InputTopic, fnTool.OutputTopic, msgID, fnTool.OutputSchema) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to set up consumer: %v", err)), nil + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{ + Text: fmt.Sprintf("Failed to set up consumer: %v", err), + }, + }, + IsError: true, + }, nil } // Wait for result or timeout select { case result := <-resultChan: if result.Error != nil { - return mcp.NewToolResultError(fmt.Sprintf("Function execution failed: %v", result.Error)), nil + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{ + Text: fmt.Sprintf("Function execution failed: %v", result.Error), + }, + }, + IsError: true, + }, nil } - return mcp.NewToolResultText(result.Data), nil + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{ + Text: result.Data, + }, + }, + }, nil case <-ctx.Done(): - return mcp.NewToolResultError(fmt.Sprintf("Function invocation timed out after %v", ctx.Value("timeout"))), nil + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{ + Text: fmt.Sprintf("Function invocation timed out after %v", ctx.Value("timeout")), + }, + }, + IsError: true, + }, nil } } diff --git a/pkg/mcp/pftools/manager.go b/pkg/mcp/pftools/manager.go index 0fef260..4dc5418 100644 --- a/pkg/mcp/pftools/manager.go +++ b/pkg/mcp/pftools/manager.go @@ -27,8 +27,7 @@ import ( "github.com/apache/pulsar-client-go/pulsaradmin/pkg/rest" "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" "github.com/google/go-cmp/cmp" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/streamnative-mcp-server/pkg/kafka" "github.com/streamnative/streamnative-mcp-server/pkg/pulsar" "github.com/streamnative/streamnative-mcp-server/pkg/schema" @@ -51,7 +50,7 @@ var DefaultStringSchemaInfo = &SchemaInfo{ // Server is imported directly to avoid circular dependency type Server struct { - MCPServer *server.MCPServer + MCPServer MCPServerInterface KafkaSession *kafka.Session PulsarSession *pulsar.Session Logger interface{} @@ -203,21 +202,27 @@ func (m *PulsarFunctionManager) updateFunctions() { if changed { if m.sessionID != "" { - err := m.mcpServer.DeleteSessionTools(m.sessionID, fnTool.Tool.Name) + err := m.mcpServer.DeleteSessionTools(m.sessionID, fnTool.Name) if err != nil { - log.Printf("Failed to delete tool %s from session %s: %v", fnTool.Tool.Name, m.sessionID, err) + log.Printf("Failed to delete tool %s from session %s: %v", fnTool.Name, m.sessionID, err) } } else { - m.mcpServer.DeleteTools(fnTool.Tool.Name) + err := m.mcpServer.DeleteTools(fnTool.Name) + if err != nil { + log.Printf("Failed to delete tool %s: %v", fnTool.Name, err) + } } } if m.sessionID != "" { err := m.mcpServer.AddSessionTool(m.sessionID, fnTool.Tool, m.handleToolCall(fnTool)) if err != nil { - log.Printf("Failed to add tool %s to session %s: %v", fnTool.Tool.Name, m.sessionID, err) + log.Printf("Failed to add tool %s to session %s: %v", fnTool.Name, m.sessionID, err) } } else { - m.mcpServer.AddTool(fnTool.Tool, m.handleToolCall(fnTool)) + err := m.mcpServer.AddTool(fnTool.Tool, m.handleToolCall(fnTool)) + if err != nil { + log.Printf("Failed to add tool %s: %v", fnTool.Name, err) + } } // Add function to map @@ -226,9 +231,9 @@ func (m *PulsarFunctionManager) updateFunctions() { m.mutex.Unlock() if changed { - log.Printf("Updated function %s as MCP tool [%s]", fullName, fnTool.Tool.Name) + log.Printf("Updated function %s as MCP tool [%s]", fullName, fnTool.Name) } else { - log.Printf("Added function %s as MCP tool [%s]", fullName, fnTool.Tool.Name) + log.Printf("Added function %s as MCP tool [%s]", fullName, fnTool.Name) } } @@ -237,15 +242,18 @@ func (m *PulsarFunctionManager) updateFunctions() { for fullName, fnTool := range m.fnToToolMap { if !seenFunctions[fullName] { if m.sessionID != "" { - err := m.mcpServer.DeleteSessionTools(m.sessionID, fnTool.Tool.Name) + err := m.mcpServer.DeleteSessionTools(m.sessionID, fnTool.Name) if err != nil { - log.Printf("Failed to delete tool %s from session %s: %v", fnTool.Tool.Name, m.sessionID, err) + log.Printf("Failed to delete tool %s from session %s: %v", fnTool.Name, m.sessionID, err) } } else { - m.mcpServer.DeleteTools(fnTool.Tool.Name) + err := m.mcpServer.DeleteTools(fnTool.Name) + if err != nil { + log.Printf("Failed to delete tool %s: %v", fnTool.Name, err) + } } delete(m.fnToToolMap, fullName) - log.Printf("Removed function %s from MCP tools [%s]", fullName, fnTool.Tool.Name) + log.Printf("Removed function %s from MCP tools [%s]", fullName, fnTool.Name) } } m.mutex.Unlock() @@ -414,12 +422,14 @@ func (m *PulsarFunctionManager) convertFunctionToTool(fn *utils.FunctionConfig) return nil, fmt.Errorf("failed to convert input schema to MCP tool input schema properties: %w", err) } - toolInputSchemaProperties = append(toolInputSchemaProperties, mcp.WithDescription(description)) - - // Create the tool - tool := mcp.NewTool(toolName, - toolInputSchemaProperties..., - ) + // Create a basic tool with description + // For now, we convert the input schema to JSON for the description + // In the future, we should set this as the tool's input schema + inputSchemaJSON, _ := json.Marshal(toolInputSchemaProperties) + tool := mcpsdk.Tool{ + Name: toolName, + Description: description + " Input schema: " + string(inputSchemaJSON), + } // Create circuit breaker for this function circuitBreaker := NewCircuitBreaker(5, 60*time.Second) @@ -442,8 +452,8 @@ func (m *PulsarFunctionManager) convertFunctionToTool(fn *utils.FunctionConfig) } // handleToolCall returns a handler function for a specific function tool -func (m *PulsarFunctionManager) handleToolCall(fnTool *FunctionTool) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func (m *PulsarFunctionManager) handleToolCall(fnTool *FunctionTool) mcpsdk.ToolHandler { + return func(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get the circuit breaker m.mutex.RLock() cb, exists := m.circuitBreakers[fnTool.Name] @@ -458,7 +468,14 @@ func (m *PulsarFunctionManager) handleToolCall(fnTool *FunctionTool) func(ctx co // Check if the circuit breaker allows the request if !cb.AllowRequest() { - return mcp.NewToolResultError(fmt.Sprintf("Circuit breaker is open for function %s. Too many failures, please try again later.", fnTool.Name)), nil + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{ + Text: fmt.Sprintf("Circuit breaker is open for function %s. Too many failures, please try again later.", fnTool.Name), + }, + }, + IsError: true, + }, nil } // Create function invoker @@ -478,8 +495,25 @@ func (m *PulsarFunctionManager) handleToolCall(fnTool *FunctionTool) func(ctx co m.mutex.Unlock() }() + // Get arguments from request + var params map[string]interface{} + if request.Params.Arguments != nil && len(request.Params.Arguments) > 0 { + if err := json.Unmarshal(request.Params.Arguments, ¶ms); err != nil { + return &mcpsdk.CallToolResult{ + Content: []mcpsdk.Content{ + &mcpsdk.TextContent{ + Text: fmt.Sprintf("Failed to parse arguments: %v", err), + }, + }, + IsError: true, + }, nil + } + } else { + params = make(map[string]interface{}) + } + // Invoke function and wait for result - result, err := invoker.InvokeFunctionAndWait(timeoutCtx, fnTool, request.GetArguments()) + result, err := invoker.InvokeFunctionAndWait(timeoutCtx, fnTool, params) // Record success or failure if err != nil { diff --git a/pkg/mcp/pftools/schema.go b/pkg/mcp/pftools/schema.go index 6fe20f2..a31147e 100644 --- a/pkg/mcp/pftools/schema.go +++ b/pkg/mcp/pftools/schema.go @@ -20,11 +20,10 @@ import ( "github.com/apache/pulsar-client-go/pulsar" "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" "github.com/streamnative/pulsarctl/pkg/cmdutils" ) -var DefaultStringSchema = &mcp.ToolInputSchema{ +var DefaultStringSchema = &ToolInputSchema{ Type: "object", Properties: map[string]interface{}{ "payload": map[string]interface{}{ @@ -78,7 +77,7 @@ func GetSchemaFromTopic(admin cmdutils.Client, topic string) (*SchemaInfo, error } // ConvertSchemaToToolInput converts a schema to MCP tool input schema -func ConvertSchemaToToolInput(schemaInfo *SchemaInfo) (*mcp.ToolInputSchema, error) { +func ConvertSchemaToToolInput(schemaInfo *SchemaInfo) (*ToolInputSchema, error) { if schemaInfo == nil { // Default to object with any fields if no schema is provided return DefaultStringSchema, nil @@ -96,7 +95,7 @@ func ConvertSchemaToToolInput(schemaInfo *SchemaInfo) (*mcp.ToolInputSchema, err } // convertComplexSchemaToToolInput handles conversion of complex schema types -func convertComplexSchemaToToolInput(schemaInfo *SchemaInfo) (*mcp.ToolInputSchema, error) { +func convertComplexSchemaToToolInput(schemaInfo *SchemaInfo) (*ToolInputSchema, error) { if schemaInfo.Definition == nil { return DefaultStringSchema, nil } @@ -112,7 +111,7 @@ func convertComplexSchemaToToolInput(schemaInfo *SchemaInfo) (*mcp.ToolInputSche } // For JSON schemas, use the definition directly - return &mcp.ToolInputSchema{ + return &ToolInputSchema{ Type: "object", Properties: map[string]interface{}{ "payload": map[string]interface{}{ diff --git a/pkg/mcp/pftools/types.go b/pkg/mcp/pftools/types.go index 93c5b4f..0b97460 100644 --- a/pkg/mcp/pftools/types.go +++ b/pkg/mcp/pftools/types.go @@ -21,11 +21,19 @@ import ( "github.com/apache/pulsar-client-go/pulsar" "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" "github.com/streamnative/pulsarctl/pkg/cmdutils" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" ) +// MCPServerInterface defines the interface for an MCP server. +// This is used by pftools to avoid circular dependency on the mcp package. +type MCPServerInterface interface { + AddTool(tool interface{}, handler interface{}) error + AddSessionTool(sessionID string, tool interface{}, handler interface{}) error + DeleteTools(name string) error + DeleteSessionTools(sessionID, name string) error +} + // PulsarFunctionManager manages the lifecycle of Pulsar Functions as MCP tools type PulsarFunctionManager struct { adminClient cmdutils.Client @@ -38,7 +46,7 @@ type PulsarFunctionManager struct { pollInterval time.Duration stopCh chan struct{} callInProgressMap map[string]context.CancelFunc - mcpServer *server.MCPServer + mcpServer MCPServerInterface readOnly bool defaultTimeout time.Duration circuitBreakers map[string]*CircuitBreaker @@ -55,7 +63,7 @@ type FunctionTool struct { OutputSchema *SchemaInfo InputTopic string OutputTopic string - Tool mcp.Tool + Tool mcpsdk.Tool SchemaFetchSuccess bool } @@ -104,3 +112,10 @@ func DefaultManagerOptions() *ManagerOptions { StrictExport: false, } } + +// ToolInputSchema represents the input schema for a tool. +// This is a local type used within pftools to avoid external dependencies. +type ToolInputSchema struct { + Type string + Properties map[string]interface{} +} diff --git a/pkg/mcp/prompts.go b/pkg/mcp/prompts.go index edec048..ebf7bd3 100644 --- a/pkg/mcp/prompts.go +++ b/pkg/mcp/prompts.go @@ -20,8 +20,7 @@ import ( "fmt" "slices" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/streamnative-mcp-server/pkg/common" context2 "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" sncloud "github.com/streamnative/streamnative-mcp-server/sdk/sdk-apiserver" @@ -59,26 +58,48 @@ var ( AvailableProviders = []string{"azure", "aws", "gcloud"} ) -func RegisterPrompts(s *server.MCPServer) { - s.AddPrompt(mcp.NewPrompt("list-sncloud-clusters", - mcp.WithPromptDescription("List all clusters from the StreamNative Cloud"), - ), HandleListPulsarClusters) - s.AddPrompt(mcp.NewPrompt("read-sncloud-cluster", - mcp.WithPromptDescription("Read a cluster from the StreamNative Cloud"), - mcp.WithArgument("name", mcp.RequiredArgument(), mcp.ArgumentDescription("The name of the cluster")), - ), handleReadPulsarCluster) - s.AddPrompt( - mcp.NewPrompt("build-sncloud-serverless-cluster", - mcp.WithPromptDescription("Build a Serverless cluster in the StreamNative Cloud"), - mcp.WithArgument("instance-name", mcp.RequiredArgument(), mcp.ArgumentDescription("The name of the Pulsar instance, cannot reuse the name of existing instance.")), - mcp.WithArgument("cluster-name", mcp.RequiredArgument(), mcp.ArgumentDescription("The name of the Pulsar cluster, cannot reuse the name of existing cluster.")), - mcp.WithArgument("provider", mcp.ArgumentDescription("The cloud provider, could be `aws`, `gcp`, `azure`. If the selected provider do not serve serverless cluster, the prompt will return an error. If not specified, the system will use a random provider depending on the availability.")), - ), - handleBuildServerlessPulsarCluster, - ) +func RegisterPrompts(s *MCPServer) { + s.AddPrompt(&mcpsdk.Prompt{ + Name: "list-sncloud-clusters", + Description: "List all clusters from the StreamNative Cloud", + }, HandleListPulsarClusters) + + s.AddPrompt(&mcpsdk.Prompt{ + Name: "read-sncloud-cluster", + Description: "Read a cluster from the StreamNative Cloud", + Arguments: []*mcpsdk.PromptArgument{ + { + Name: "name", + Description: "The name of the cluster", + Required: true, + }, + }, + }, handleReadPulsarCluster) + + s.AddPrompt(&mcpsdk.Prompt{ + Name: "build-sncloud-serverless-cluster", + Description: "Build a Serverless cluster in the StreamNative Cloud", + Arguments: []*mcpsdk.PromptArgument{ + { + Name: "instance-name", + Description: "The name of the Pulsar instance, cannot reuse the name of existing instance.", + Required: true, + }, + { + Name: "cluster-name", + Description: "The name of the Pulsar cluster, cannot reuse the name of existing cluster.", + Required: true, + }, + { + Name: "provider", + Description: "The cloud provider, could be `aws`, `gcp`, `azure`. If the selected provider do not serve serverless cluster, the prompt will return an error. If not specified, the system will use a random provider depending on the availability.", + Required: false, + }, + }, + }, handleBuildServerlessPulsarCluster) } -func HandleListPulsarClusters(ctx context.Context, _ mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { +func HandleListPulsarClusters(ctx context.Context, _ *mcpsdk.GetPromptRequest) (*mcpsdk.GetPromptResult, error) { // Get API client from session session := context2.GetSNCloudSession(ctx) if session == nil { @@ -97,20 +118,19 @@ func HandleListPulsarClusters(ctx context.Context, _ mcp.GetPromptRequest) (*mcp defer clustersBody.Body.Close() var messages = make( - []mcp.PromptMessage, + []*mcpsdk.PromptMessage, len(clusters.Items)+1, ) - messages[0] = mcp.PromptMessage{ - Content: mcp.TextContent{ - Type: "text", + messages[0] = &mcpsdk.PromptMessage{ + Content: &mcpsdk.TextContent{ Text: fmt.Sprintf( "There are %d Pulsar clusters in the StreamNative Cloud from organization %s:", len(clusters.Items), session.Ctx.Organization, ), }, - Role: mcp.RoleUser, + Role: "user", } for i, cluster := range clusters.Items { @@ -127,9 +147,8 @@ func HandleListPulsarClusters(ctx context.Context, _ mcp.GetPromptRequest) (*mcp engineType := common.GetEngineType(cluster) - messages[i+1] = mcp.PromptMessage{ - Content: mcp.TextContent{ - Type: "text", + messages[i+1] = &mcpsdk.PromptMessage{ + Content: &mcpsdk.TextContent{ Text: fmt.Sprintf( "Instance Name: %s\nCluster Name: %s\nCluster Display Name: %s\nCluster Status: %s\nCluster Engine Type: %s", instanceName, @@ -139,17 +158,17 @@ func HandleListPulsarClusters(ctx context.Context, _ mcp.GetPromptRequest) (*mcp engineType, ), }, - Role: mcp.RoleUser, + Role: "user", } } - return &mcp.GetPromptResult{ + return &mcpsdk.GetPromptResult{ Description: fmt.Sprintf("Pulsar clusters from StreamNative Cloud organization %s, you can use `sncloud_context_use_cluster` tool to switch to selected cluster, and use pulsar and kafka tools to interact with the cluster.", session.Ctx.Organization), Messages: messages, }, nil } -func handleReadPulsarCluster(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { +func handleReadPulsarCluster(ctx context.Context, request *mcpsdk.GetPromptRequest) (*mcpsdk.GetPromptResult, error) { // Get API client from session session := context2.GetSNCloudSession(ctx) if session == nil { @@ -161,7 +180,13 @@ func handleReadPulsarCluster(ctx context.Context, request mcp.GetPromptRequest) return nil, fmt.Errorf("failed to get API client: %v", err) } - name, err := common.RequiredParam[string](common.ConvertToMapInterface(request.Params.Arguments), "name") + // Convert map[string]string to map[string]interface{} for common.RequiredParam + args := make(map[string]interface{}, len(request.Params.Arguments)) + for k, v := range request.Params.Arguments { + args[k] = v + } + + name, err := common.RequiredParam[string](args, "name") if err != nil { return nil, fmt.Errorf("failed to get name: %v", err) } @@ -193,25 +218,24 @@ func handleReadPulsarCluster(ctx context.Context, request mcp.GetPromptRequest) } var messages = make( - []mcp.PromptMessage, + []*mcpsdk.PromptMessage, 1, ) - messages[0] = mcp.PromptMessage{ - Content: mcp.TextContent{ - Type: "text", + messages[0] = &mcpsdk.PromptMessage{ + Content: &mcpsdk.TextContent{ Text: string(context), }, - Role: mcp.RoleUser, + Role: "user", } - return &mcp.GetPromptResult{ + return &mcpsdk.GetPromptResult{ Description: fmt.Sprintf("Detailed information of Pulsar cluster %s, you can use `sncloud_context_use_cluster` tool to switch to this cluster, and use pulsar and kafka tools to interact with the cluster.", name), Messages: messages, }, nil } -func handleBuildServerlessPulsarCluster(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { +func handleBuildServerlessPulsarCluster(ctx context.Context, request *mcpsdk.GetPromptRequest) (*mcpsdk.GetPromptResult, error) { // Get API client from session session := context2.GetSNCloudSession(ctx) if session == nil { @@ -222,19 +246,24 @@ func handleBuildServerlessPulsarCluster(ctx context.Context, request mcp.GetProm if err != nil { return nil, fmt.Errorf("failed to get API client: %v", err) } - arguments := common.ConvertToMapInterface(request.Params.Arguments) - instanceName, err := common.RequiredParam[string](arguments, "instance-name") + // Convert map[string]string to map[string]interface{} for common.RequiredParam + args := make(map[string]interface{}, len(request.Params.Arguments)) + for k, v := range request.Params.Arguments { + args[k] = v + } + + instanceName, err := common.RequiredParam[string](args, "instance-name") if err != nil { return nil, fmt.Errorf("failed to get instance name: %v", err) } - clusterName, err := common.RequiredParam[string](arguments, "cluster-name") + clusterName, err := common.RequiredParam[string](args, "cluster-name") if err != nil { return nil, fmt.Errorf("failed to get cluster name: %v", err) } - provider, hasProvider := common.OptionalParam[string](arguments, "provider") + provider, hasProvider := common.OptionalParam[string](args, "provider") if !hasProvider { provider = "" } @@ -331,31 +360,28 @@ func handleBuildServerlessPulsarCluster(ctx context.Context, request mcp.GetProm return nil, fmt.Errorf("failed to marshal cluster: %v", err) } - messages := []mcp.PromptMessage{ + messages := []*mcpsdk.PromptMessage{ { - Content: mcp.TextContent{ - Type: "text", + Content: &mcpsdk.TextContent{ Text: "The following is the Pulsar instance JSON definition and the Pulsar cluster JSON definition, you can use the `sncloud_resources_apply` tool to apply the resources to the StreamNative Cloud. Please directly use the JSON content and not modify the content. The PulsarCluster name is required to be empty. You will need to apply PulsarInstance first, then apply PulsarCluster.", }, - Role: mcp.RoleUser, + Role: "user", }, { - Content: mcp.TextContent{ - Type: "text", + Content: &mcpsdk.TextContent{ Text: string(instJSON), }, - Role: mcp.RoleUser, + Role: "user", }, { - Content: mcp.TextContent{ - Type: "text", + Content: &mcpsdk.TextContent{ Text: string(clusJSON), }, - Role: mcp.RoleUser, + Role: "user", }, } - return &mcp.GetPromptResult{ + return &mcpsdk.GetPromptResult{ Description: fmt.Sprintf("Create a new Serverless Pulsar cluster %s's related resources that can be applied to the StreamNative Cloud.", clusterName), Messages: messages, }, nil diff --git a/pkg/mcp/pulsar_admin_brokers_stats_tools.go b/pkg/mcp/pulsar_admin_brokers_stats_tools.go index d50b398..9c6592f 100644 --- a/pkg/mcp/pulsar_admin_brokers_stats_tools.go +++ b/pkg/mcp/pulsar_admin_brokers_stats_tools.go @@ -17,13 +17,12 @@ package mcp import ( "context" - "github.com/mark3labs/mcp-go/server" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" pulsarbuilders "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders/pulsar" ) // PulsarAdminAddBrokerStatsTools adds broker-stats related tools to the MCP server -func PulsarAdminAddBrokerStatsTools(s *server.MCPServer, readOnly bool, features []string) { +func PulsarAdminAddBrokerStatsTools(s *MCPServer, readOnly bool, features []string) { // Use the new builder pattern builder := pulsarbuilders.NewPulsarAdminBrokerStatsToolBuilder() config := builders.ToolBuildConfig{ diff --git a/pkg/mcp/pulsar_admin_brokers_tools.go b/pkg/mcp/pulsar_admin_brokers_tools.go index 6ce43ed..f3c8cf0 100644 --- a/pkg/mcp/pulsar_admin_brokers_tools.go +++ b/pkg/mcp/pulsar_admin_brokers_tools.go @@ -17,13 +17,12 @@ package mcp import ( "context" - "github.com/mark3labs/mcp-go/server" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" pulsarbuilders "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders/pulsar" ) // PulsarAdminAddBrokersTools adds broker-related tools to the MCP server -func PulsarAdminAddBrokersTools(s *server.MCPServer, readOnly bool, features []string) { +func PulsarAdminAddBrokersTools(s *MCPServer, readOnly bool, features []string) { // Use the new builder pattern builder := pulsarbuilders.NewPulsarAdminBrokersToolBuilder() config := builders.ToolBuildConfig{ diff --git a/pkg/mcp/pulsar_admin_cluster_tools.go b/pkg/mcp/pulsar_admin_cluster_tools.go index 51f1f38..883e047 100644 --- a/pkg/mcp/pulsar_admin_cluster_tools.go +++ b/pkg/mcp/pulsar_admin_cluster_tools.go @@ -17,13 +17,12 @@ package mcp import ( "context" - "github.com/mark3labs/mcp-go/server" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" pulsarBuilders "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders/pulsar" ) // PulsarAdminClusterTools creates Pulsar Admin Cluster tool list using the new builder pattern -func PulsarAdminClusterTools(readOnly bool, features []string) []server.ServerTool { +func PulsarAdminClusterTools(readOnly bool, features []string) []builders.ServerTool { builder := pulsarBuilders.NewPulsarAdminClusterToolBuilder() config := builders.ToolBuildConfig{ ReadOnly: readOnly, @@ -40,7 +39,7 @@ func PulsarAdminClusterTools(readOnly bool, features []string) []server.ServerTo } // PulsarAdminAddClusterTools adds cluster-related tools to the MCP server -func PulsarAdminAddClusterTools(s *server.MCPServer, readOnly bool, features []string) { +func PulsarAdminAddClusterTools(s *MCPServer, readOnly bool, features []string) { tools := PulsarAdminClusterTools(readOnly, features) for _, tool := range tools { diff --git a/pkg/mcp/pulsar_admin_functions_tools.go b/pkg/mcp/pulsar_admin_functions_tools.go index 2b9203e..3ea00e5 100644 --- a/pkg/mcp/pulsar_admin_functions_tools.go +++ b/pkg/mcp/pulsar_admin_functions_tools.go @@ -17,13 +17,12 @@ package mcp import ( "context" - "github.com/mark3labs/mcp-go/server" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" pulsarbuilders "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders/pulsar" ) // PulsarAdminAddFunctionsTools adds a unified function-related tool to the MCP server -func PulsarAdminAddFunctionsTools(s *server.MCPServer, readOnly bool, features []string) { +func PulsarAdminAddFunctionsTools(s *MCPServer, readOnly bool, features []string) { // Use the new builder pattern builder := pulsarbuilders.NewPulsarAdminFunctionsToolBuilder() config := builders.ToolBuildConfig{ diff --git a/pkg/mcp/pulsar_admin_functions_worker_tools.go b/pkg/mcp/pulsar_admin_functions_worker_tools.go index 1a4a428..e3d44bf 100644 --- a/pkg/mcp/pulsar_admin_functions_worker_tools.go +++ b/pkg/mcp/pulsar_admin_functions_worker_tools.go @@ -18,13 +18,12 @@ import ( "context" "fmt" - "github.com/mark3labs/mcp-go/server" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" pulsarBuilders "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders/pulsar" ) // PulsarAdminFunctionsWorkerTools creates Pulsar Admin Functions Worker tool list using the new builder pattern -func PulsarAdminFunctionsWorkerTools(readOnly bool, features []string) []server.ServerTool { +func PulsarAdminFunctionsWorkerTools(readOnly bool, features []string) []builders.ServerTool { builder := pulsarBuilders.NewPulsarAdminFunctionsWorkerToolBuilder() config := builders.ToolBuildConfig{ ReadOnly: readOnly, @@ -42,7 +41,7 @@ func PulsarAdminFunctionsWorkerTools(readOnly bool, features []string) []server. } // PulsarAdminAddFunctionsWorkerTools adds functions worker-related tools to the MCP server -func PulsarAdminAddFunctionsWorkerTools(s *server.MCPServer, readOnly bool, features []string) { +func PulsarAdminAddFunctionsWorkerTools(s *MCPServer, readOnly bool, features []string) { tools := PulsarAdminFunctionsWorkerTools(readOnly, features) for _, tool := range tools { diff --git a/pkg/mcp/pulsar_admin_namespace_policy_tools.go b/pkg/mcp/pulsar_admin_namespace_policy_tools.go index c74cfe0..e06b45c 100644 --- a/pkg/mcp/pulsar_admin_namespace_policy_tools.go +++ b/pkg/mcp/pulsar_admin_namespace_policy_tools.go @@ -17,13 +17,12 @@ package mcp import ( "context" - "github.com/mark3labs/mcp-go/server" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" pulsarbuilders "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders/pulsar" ) // PulsarAdminAddNamespacePolicyTools adds namespace policy-related tools to the MCP server -func PulsarAdminAddNamespacePolicyTools(s *server.MCPServer, readOnly bool, features []string) { +func PulsarAdminAddNamespacePolicyTools(s *MCPServer, readOnly bool, features []string) { // Use the new builder pattern builder := pulsarbuilders.NewPulsarAdminNamespacePolicyToolBuilder() config := builders.ToolBuildConfig{ diff --git a/pkg/mcp/pulsar_admin_namespace_tools.go b/pkg/mcp/pulsar_admin_namespace_tools.go index a31b0e3..06997b1 100644 --- a/pkg/mcp/pulsar_admin_namespace_tools.go +++ b/pkg/mcp/pulsar_admin_namespace_tools.go @@ -18,13 +18,12 @@ import ( "context" "fmt" - "github.com/mark3labs/mcp-go/server" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" pulsarBuilders "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders/pulsar" ) // PulsarAdminNamespaceTools creates Pulsar Admin Namespace tool list using the new builder pattern -func PulsarAdminNamespaceTools(readOnly bool, features []string) []server.ServerTool { +func PulsarAdminNamespaceTools(readOnly bool, features []string) []builders.ServerTool { builder := pulsarBuilders.NewPulsarAdminNamespaceToolBuilder() config := builders.ToolBuildConfig{ ReadOnly: readOnly, @@ -42,7 +41,7 @@ func PulsarAdminNamespaceTools(readOnly bool, features []string) []server.Server } // PulsarAdminAddNamespaceTools adds namespace-related tools to the MCP server -func PulsarAdminAddNamespaceTools(s *server.MCPServer, readOnly bool, features []string) { +func PulsarAdminAddNamespaceTools(s *MCPServer, readOnly bool, features []string) { tools := PulsarAdminNamespaceTools(readOnly, features) for _, tool := range tools { diff --git a/pkg/mcp/pulsar_admin_nsisolationpolicy_tools.go b/pkg/mcp/pulsar_admin_nsisolationpolicy_tools.go index f742f6c..3e16878 100644 --- a/pkg/mcp/pulsar_admin_nsisolationpolicy_tools.go +++ b/pkg/mcp/pulsar_admin_nsisolationpolicy_tools.go @@ -17,13 +17,12 @@ package mcp import ( "context" - "github.com/mark3labs/mcp-go/server" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" pulsarbuilders "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders/pulsar" ) // PulsarAdminAddNsIsolationPolicyTools adds namespace isolation policy related tools to the MCP server -func PulsarAdminAddNsIsolationPolicyTools(s *server.MCPServer, readOnly bool, features []string) { +func PulsarAdminAddNsIsolationPolicyTools(s *MCPServer, readOnly bool, features []string) { // Use the new builder pattern builder := pulsarbuilders.NewPulsarAdminNsIsolationPolicyToolBuilder() config := builders.ToolBuildConfig{ diff --git a/pkg/mcp/pulsar_admin_packages_tools.go b/pkg/mcp/pulsar_admin_packages_tools.go index bfa929d..8c66437 100644 --- a/pkg/mcp/pulsar_admin_packages_tools.go +++ b/pkg/mcp/pulsar_admin_packages_tools.go @@ -18,7 +18,6 @@ import ( "context" "strings" - "github.com/mark3labs/mcp-go/server" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" pulsarbuilders "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders/pulsar" ) @@ -45,7 +44,7 @@ func IsPackageURLSupported(functionPkgURL string) bool { } // PulsarAdminAddPackagesTools adds package-related tools to the MCP server -func PulsarAdminAddPackagesTools(s *server.MCPServer, readOnly bool, features []string) { +func PulsarAdminAddPackagesTools(s *MCPServer, readOnly bool, features []string) { // Use the new builder pattern builder := pulsarbuilders.NewPulsarAdminPackagesToolBuilder() config := builders.ToolBuildConfig{ diff --git a/pkg/mcp/pulsar_admin_resourcequotas_tools.go b/pkg/mcp/pulsar_admin_resourcequotas_tools.go index 6382d0c..f3c1955 100644 --- a/pkg/mcp/pulsar_admin_resourcequotas_tools.go +++ b/pkg/mcp/pulsar_admin_resourcequotas_tools.go @@ -17,13 +17,12 @@ package mcp import ( "context" - "github.com/mark3labs/mcp-go/server" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" pulsarbuilders "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders/pulsar" ) // PulsarAdminAddResourceQuotasTools adds resource quotas-related tools to the MCP server -func PulsarAdminAddResourceQuotasTools(s *server.MCPServer, readOnly bool, features []string) { +func PulsarAdminAddResourceQuotasTools(s *MCPServer, readOnly bool, features []string) { // Use the new builder pattern builder := pulsarbuilders.NewPulsarAdminResourceQuotasToolBuilder() config := builders.ToolBuildConfig{ diff --git a/pkg/mcp/pulsar_admin_schemas_tools.go b/pkg/mcp/pulsar_admin_schemas_tools.go index fd8ea01..9feede0 100644 --- a/pkg/mcp/pulsar_admin_schemas_tools.go +++ b/pkg/mcp/pulsar_admin_schemas_tools.go @@ -18,13 +18,12 @@ import ( "context" "fmt" - "github.com/mark3labs/mcp-go/server" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" pulsarBuilders "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders/pulsar" ) // PulsarAdminSchemaTools creates Pulsar Admin Schema tool list using the new builder pattern -func PulsarAdminSchemaTools(readOnly bool, features []string) []server.ServerTool { +func PulsarAdminSchemaTools(readOnly bool, features []string) []builders.ServerTool { builder := pulsarBuilders.NewPulsarAdminSchemaToolBuilder() config := builders.ToolBuildConfig{ ReadOnly: readOnly, @@ -42,7 +41,7 @@ func PulsarAdminSchemaTools(readOnly bool, features []string) []server.ServerToo } // PulsarAdminAddSchemasTools adds schema-related tools to the MCP server -func PulsarAdminAddSchemasTools(s *server.MCPServer, readOnly bool, features []string) { +func PulsarAdminAddSchemasTools(s *MCPServer, readOnly bool, features []string) { tools := PulsarAdminSchemaTools(readOnly, features) for _, tool := range tools { diff --git a/pkg/mcp/pulsar_admin_sinks_tools.go b/pkg/mcp/pulsar_admin_sinks_tools.go index cc4abac..fd70ed1 100644 --- a/pkg/mcp/pulsar_admin_sinks_tools.go +++ b/pkg/mcp/pulsar_admin_sinks_tools.go @@ -17,13 +17,12 @@ package mcp import ( "context" - "github.com/mark3labs/mcp-go/server" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" pulsarbuilders "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders/pulsar" ) // PulsarAdminAddSinksTools adds a unified sink-related tool to the MCP server -func PulsarAdminAddSinksTools(s *server.MCPServer, readOnly bool, features []string) { +func PulsarAdminAddSinksTools(s *MCPServer, readOnly bool, features []string) { // Use the new builder pattern builder := pulsarbuilders.NewPulsarAdminSinksToolBuilder() config := builders.ToolBuildConfig{ diff --git a/pkg/mcp/pulsar_admin_sources_tools.go b/pkg/mcp/pulsar_admin_sources_tools.go index 8abc646..37ba2ff 100644 --- a/pkg/mcp/pulsar_admin_sources_tools.go +++ b/pkg/mcp/pulsar_admin_sources_tools.go @@ -17,13 +17,12 @@ package mcp import ( "context" - "github.com/mark3labs/mcp-go/server" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" pulsarbuilders "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders/pulsar" ) // PulsarAdminAddSourcesTools adds a unified source-related tool to the MCP server -func PulsarAdminAddSourcesTools(s *server.MCPServer, readOnly bool, features []string) { +func PulsarAdminAddSourcesTools(s *MCPServer, readOnly bool, features []string) { // Use the new builder pattern builder := pulsarbuilders.NewPulsarAdminSourcesToolBuilder() config := builders.ToolBuildConfig{ diff --git a/pkg/mcp/pulsar_admin_subscription_tools.go b/pkg/mcp/pulsar_admin_subscription_tools.go index 2a7d03e..4b999c1 100644 --- a/pkg/mcp/pulsar_admin_subscription_tools.go +++ b/pkg/mcp/pulsar_admin_subscription_tools.go @@ -18,13 +18,12 @@ import ( "context" "fmt" - "github.com/mark3labs/mcp-go/server" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" pulsarBuilders "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders/pulsar" ) // PulsarAdminSubscriptionTools creates Pulsar Admin Subscription tool list using the new builder pattern -func PulsarAdminSubscriptionTools(readOnly bool, features []string) []server.ServerTool { +func PulsarAdminSubscriptionTools(readOnly bool, features []string) []builders.ServerTool { builder := pulsarBuilders.NewPulsarAdminSubscriptionToolBuilder() config := builders.ToolBuildConfig{ ReadOnly: readOnly, @@ -42,7 +41,7 @@ func PulsarAdminSubscriptionTools(readOnly bool, features []string) []server.Ser } // PulsarAdminAddSubscriptionTools adds subscription-related tools to the MCP server -func PulsarAdminAddSubscriptionTools(s *server.MCPServer, readOnly bool, features []string) { +func PulsarAdminAddSubscriptionTools(s *MCPServer, readOnly bool, features []string) { tools := PulsarAdminSubscriptionTools(readOnly, features) for _, tool := range tools { diff --git a/pkg/mcp/pulsar_admin_tenant_tools.go b/pkg/mcp/pulsar_admin_tenant_tools.go index 480ca24..f816090 100644 --- a/pkg/mcp/pulsar_admin_tenant_tools.go +++ b/pkg/mcp/pulsar_admin_tenant_tools.go @@ -17,12 +17,11 @@ package mcp import ( "context" - "github.com/mark3labs/mcp-go/server" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" pulsarbuilders "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders/pulsar" ) -func PulsarAdminAddTenantTools(s *server.MCPServer, readOnly bool, features []string) { +func PulsarAdminAddTenantTools(s *MCPServer, readOnly bool, features []string) { // Use the new builder pattern builder := pulsarbuilders.NewPulsarAdminTenantToolBuilder() config := builders.ToolBuildConfig{ diff --git a/pkg/mcp/pulsar_admin_topic_policy_tools.go b/pkg/mcp/pulsar_admin_topic_policy_tools.go index 7eb1b1c..8ebda28 100644 --- a/pkg/mcp/pulsar_admin_topic_policy_tools.go +++ b/pkg/mcp/pulsar_admin_topic_policy_tools.go @@ -17,13 +17,12 @@ package mcp import ( "context" - "github.com/mark3labs/mcp-go/server" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" pulsarbuilders "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders/pulsar" ) // PulsarAdminAddTopicPolicyTools adds topic policy-related tools to the MCP server -func PulsarAdminAddTopicPolicyTools(s *server.MCPServer, readOnly bool, features []string) { +func PulsarAdminAddTopicPolicyTools(s *MCPServer, readOnly bool, features []string) { // Use the new builder pattern builder := pulsarbuilders.NewPulsarAdminTopicPolicyToolBuilder() config := builders.ToolBuildConfig{ diff --git a/pkg/mcp/pulsar_admin_topic_tools.go b/pkg/mcp/pulsar_admin_topic_tools.go index c02fc64..660e31a 100644 --- a/pkg/mcp/pulsar_admin_topic_tools.go +++ b/pkg/mcp/pulsar_admin_topic_tools.go @@ -18,13 +18,12 @@ import ( "context" "fmt" - "github.com/mark3labs/mcp-go/server" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" pulsarBuilders "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders/pulsar" ) // PulsarAdminTopicTools creates Pulsar Admin Topic tool list using the new builder pattern -func PulsarAdminTopicTools(readOnly bool, features []string) []server.ServerTool { +func PulsarAdminTopicTools(readOnly bool, features []string) []builders.ServerTool { builder := pulsarBuilders.NewPulsarAdminTopicToolBuilder() config := builders.ToolBuildConfig{ ReadOnly: readOnly, @@ -42,7 +41,7 @@ func PulsarAdminTopicTools(readOnly bool, features []string) []server.ServerTool } // PulsarAdminAddTopicTools adds topic-related tools to the MCP server -func PulsarAdminAddTopicTools(s *server.MCPServer, readOnly bool, features []string) { +func PulsarAdminAddTopicTools(s *MCPServer, readOnly bool, features []string) { tools := PulsarAdminTopicTools(readOnly, features) for _, tool := range tools { diff --git a/pkg/mcp/pulsar_client_consume_tools.go b/pkg/mcp/pulsar_client_consume_tools.go index 7e68f33..bb196ff 100644 --- a/pkg/mcp/pulsar_client_consume_tools.go +++ b/pkg/mcp/pulsar_client_consume_tools.go @@ -18,13 +18,12 @@ import ( "context" "fmt" - "github.com/mark3labs/mcp-go/server" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" pulsarBuilders "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders/pulsar" ) // PulsarClientConsumeTools creates Pulsar Client Consumer tool list using the new builder pattern -func PulsarClientConsumeTools(readOnly bool, features []string) []server.ServerTool { +func PulsarClientConsumeTools(readOnly bool, features []string) []builders.ServerTool { builder := pulsarBuilders.NewPulsarClientConsumeToolBuilder() config := builders.ToolBuildConfig{ ReadOnly: readOnly, @@ -42,7 +41,7 @@ func PulsarClientConsumeTools(readOnly bool, features []string) []server.ServerT } // PulsarClientAddConsumerTools adds Pulsar client consumer tools to the MCP server -func PulsarClientAddConsumerTools(s *server.MCPServer, readOnly bool, features []string) { +func PulsarClientAddConsumerTools(s *MCPServer, readOnly bool, features []string) { tools := PulsarClientConsumeTools(readOnly, features) for _, tool := range tools { diff --git a/pkg/mcp/pulsar_client_produce_tools.go b/pkg/mcp/pulsar_client_produce_tools.go index 15a5a84..e3a3888 100644 --- a/pkg/mcp/pulsar_client_produce_tools.go +++ b/pkg/mcp/pulsar_client_produce_tools.go @@ -18,13 +18,12 @@ import ( "context" "fmt" - "github.com/mark3labs/mcp-go/server" "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" pulsarBuilders "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders/pulsar" ) // PulsarClientProduceTools creates Pulsar Client Producer tool list using the new builder pattern -func PulsarClientProduceTools(readOnly bool, features []string) []server.ServerTool { +func PulsarClientProduceTools(readOnly bool, features []string) []builders.ServerTool { builder := pulsarBuilders.NewPulsarClientProduceToolBuilder() config := builders.ToolBuildConfig{ ReadOnly: readOnly, @@ -42,7 +41,7 @@ func PulsarClientProduceTools(readOnly bool, features []string) []server.ServerT } // PulsarClientAddProducerTools adds Pulsar client producer tools to the MCP server -func PulsarClientAddProducerTools(s *server.MCPServer, readOnly bool, features []string) { +func PulsarClientAddProducerTools(s *MCPServer, readOnly bool, features []string) { tools := PulsarClientProduceTools(readOnly, features) for _, tool := range tools { diff --git a/pkg/mcp/pulsar_functions_as_tools.go b/pkg/mcp/pulsar_functions_as_tools.go index 4442fe7..f355067 100644 --- a/pkg/mcp/pulsar_functions_as_tools.go +++ b/pkg/mcp/pulsar_functions_as_tools.go @@ -116,7 +116,7 @@ func (s *Server) PulsarFunctionManagedMcpTools(readOnly bool, features []string, // Convert Server to the internal pftools.Server type pftoolsServer := &pftools2.Server{ - MCPServer: s.MCPServer, + MCPServer: s, KafkaSession: s.KafkaSession, PulsarSession: s.PulsarSession, Logger: s.logger, diff --git a/pkg/mcp/server.go b/pkg/mcp/server.go index 21674ca..aeb3f8e 100644 --- a/pkg/mcp/server.go +++ b/pkg/mcp/server.go @@ -15,42 +15,34 @@ package mcp import ( - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/sirupsen/logrus" "github.com/streamnative/streamnative-mcp-server/pkg/config" "github.com/streamnative/streamnative-mcp-server/pkg/kafka" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" "github.com/streamnative/streamnative-mcp-server/pkg/pulsar" ) type Server struct { - MCPServer *server.MCPServer + Server *mcpsdk.Server KafkaSession *kafka.Session PulsarSession *pulsar.Session SNCloudSession *config.Session logger *logrus.Logger } -func NewServer(name, version string, logger *logrus.Logger, opts ...server.ServerOption) *Server { - // Create a new MCP server - opts = AddOpts(opts...) - s := server.NewMCPServer(name, version, opts...) - mcpserver := CreateSNCloudMCPServer(s, logger) - return mcpserver -} - -func AddOpts(opts ...server.ServerOption) []server.ServerOption { - defaultOpts := []server.ServerOption{ - server.WithResourceCapabilities(true, true), - server.WithRecovery(), - server.WithLogging(), - } - opts = append(defaultOpts, opts...) - return opts -} +// NewServer creates a new MCP server using go-sdk. +// The instructions parameter provides server description for clients. +func NewServer(name, version string, logger *logrus.Logger, instructions string) *Server { + sdkServer := mcpsdk.NewServer(&mcpsdk.Implementation{ + Name: name, + Version: version, + }, &mcpsdk.ServerOptions{ + Instructions: instructions, + }) -func CreateSNCloudMCPServer(s *server.MCPServer, logger *logrus.Logger) *Server { mcpserver := &Server{ - MCPServer: s, + Server: sdkServer, logger: logger, SNCloudSession: &config.Session{}, KafkaSession: &kafka.Session{}, @@ -59,3 +51,57 @@ func CreateSNCloudMCPServer(s *server.MCPServer, logger *logrus.Logger) *Server return mcpserver } + +// AddTool adds a tool to the server with backward compatibility. +// This method accepts both mark3labs-style and go-sdk-style handlers. +func (s *Server) AddTool(tool interface{}, handler interface{}) error { + // Adapt handler to go-sdk signature + adaptedHandler := adapter.AdaptHandlerV1ToV2(handler) + + // Convert tool to *mcpsdk.Tool if it's not already + var mcpTool *mcpsdk.Tool + switch t := tool.(type) { + case *mcpsdk.Tool: + mcpTool = t + default: + // For any other type, try to use it as-is (should be *mcpsdk.Tool from builders) + mcpTool = t.(*mcpsdk.Tool) + } + + s.Server.AddTool(mcpTool, adaptedHandler) + return nil +} + +// AddSessionTool adds a session-scoped tool (used by PFTools). +func (s *Server) AddSessionTool(sessionID string, tool interface{}, handler interface{}) error { + // For go-sdk, session-scoped tools are handled differently + // The actual implementation will be in Phase 2 when we migrate PFTools + adaptedHandler := adapter.AdaptHandlerV1ToV2(handler) + + // Convert tool to *mcpsdk.Tool if it's not already + var mcpTool *mcpsdk.Tool + switch t := tool.(type) { + case *mcpsdk.Tool: + mcpTool = t + default: + // For any other type, try to use it as-is (should be *mcpsdk.Tool from builders) + mcpTool = t.(*mcpsdk.Tool) + } + + s.Server.AddTool(mcpTool, adaptedHandler) + return nil +} + +// DeleteTools removes tools by name prefix. +func (s *Server) DeleteTools(name string) error { + // go-sdk doesn't have a direct DeleteTools method + // This will be implemented in Phase 2 when we migrate PFTools + return nil +} + +// DeleteSessionTools removes session-scoped tools. +func (s *Server) DeleteSessionTools(sessionID, name string) error { + // go-sdk doesn't have a direct DeleteSessionTools method + // This will be implemented in Phase 2 when we migrate PFTools + return nil +} diff --git a/pkg/mcp/sncontext_tools.go b/pkg/mcp/sncontext_tools.go index 8658cb7..1ae0d99 100644 --- a/pkg/mcp/sncontext_tools.go +++ b/pkg/mcp/sncontext_tools.go @@ -20,32 +20,33 @@ import ( "fmt" "slices" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/streamnative-mcp-server/pkg/auth/store" "github.com/streamnative/streamnative-mcp-server/pkg/common" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" ) -func RegisterContextTools(s *server.MCPServer, features []string, skipContextTools bool) { +func RegisterContextTools(s *MCPServer, features []string, skipContextTools bool) { if !slices.Contains(features, string(FeatureStreamNativeCloud)) && !slices.Contains(features, string(FeatureAll)) { return } // Add whoami tool - whoamiTool := mcp.NewTool("sncloud_context_whoami", - mcp.WithDescription("Display the currently logged-in service account. "+ + whoamiTool := builders.NewTool("sncloud_context_whoami", + builders.WithDescription("Display the currently logged-in service account. "+ "Returns the name of the authenticated service account and the organization."), ) s.AddTool(whoamiTool, handleWhoami) // Add set-context tool - setContextTool := mcp.NewTool("sncloud_context_use_cluster", - mcp.WithDescription("Set the current context to a specific StreamNative Cloud cluster, once you set the context, you can use pulsar and kafka tools to interact with the cluster. If you encounter ContextNotSetErr, please use `sncloud_context_available_clusters` to list the available clusters and set the context to a specific cluster."), - mcp.WithString("instanceName", mcp.Required(), - mcp.Description("The name of the pulsar instance to use"), + setContextTool := builders.NewTool("sncloud_context_use_cluster", + builders.WithDescription("Set the current context to a specific StreamNative Cloud cluster, once you set the context, you can use pulsar and kafka tools to interact with the cluster. If you encounter ContextNotSetErr, please use `sncloud_context_available_clusters` to list the available clusters and set the context to a specific cluster."), + builders.WithString("instanceName", builders.Required(), + builders.Description("The name of the pulsar instance to use"), ), - mcp.WithString("clusterName", mcp.Required(), - mcp.Description("The name of the pulsar cluster to use"), + builders.WithString("clusterName", builders.Required(), + builders.Description("The name of the pulsar cluster to use"), ), ) // Skip registering context tools if context is already provided @@ -54,23 +55,23 @@ func RegisterContextTools(s *server.MCPServer, features []string, skipContextToo } // Add available-contexts tool - availableContextsTool := mcp.NewTool("sncloud_context_available_clusters", - mcp.WithDescription("Display the available pulsar clusters for the current organization on StreamNative Cloud. You can use `sncloud_context_use_cluster` to change the context to a specific cluster. You will need to ask for the USER to confirm the target context cluster if there are multiple clusters."), + availableContextsTool := builders.NewTool("sncloud_context_available_clusters", + builders.WithDescription("Display the available pulsar clusters for the current organization on StreamNative Cloud. You can use `sncloud_context_use_cluster` to change the context to a specific cluster. You will need to ask for the USER to confirm the target context cluster if there are multiple clusters."), ) s.AddTool(availableContextsTool, handleAvailableContexts) } // handleWhoami handles the whoami tool request -func handleWhoami(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func handleWhoami(ctx context.Context, _ *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { options := common.GetOptions(ctx) issuer := options.LoadConfigOrDie().Auth.Issuer() userName, err := options.WhoAmI(issuer.Audience) if err != nil { if err == store.ErrNoAuthenticationData { - return mcp.NewToolResultText("Not logged in."), nil + return adapter.NewTextResult("Not logged in."), nil } - return mcp.NewToolResultError(fmt.Sprintf("Failed to get user information: %v", err)), nil + return adapter.NewErrorResult("Failed to get user information: %v", err), nil } response := struct { @@ -83,47 +84,50 @@ func handleWhoami(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResu jsonResponse, err := json.Marshal(response) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal response: %v", err)), nil + return adapter.NewErrorResult("Failed to marshal response: %v", err), nil } - return mcp.NewToolResultText(string(jsonResponse)), nil + return adapter.NewTextResult(string(jsonResponse)), nil } // handleSetContext handles the set-context tool request -func handleSetContext(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func handleSetContext(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { options := common.GetOptions(ctx) - instanceName, err := request.RequireString("instanceName") + instanceName, err := adapter.RequireString(request, "instanceName") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get instance name: %v", err)), nil + return adapter.NewErrorResult("Failed to get instance name: %v", err), nil } - clusterName, err := request.RequireString("clusterName") + clusterName, err := adapter.RequireString(request, "clusterName") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get cluster name: %v", err)), nil + return adapter.NewErrorResult("Failed to get cluster name: %v", err), nil } err = SetContext(ctx, options, instanceName, clusterName) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to set context: %v", err)), nil + return adapter.NewErrorResult("Failed to set context: %v", err), nil } - return mcp.NewToolResultText("StreamNative Cloud context set successfully"), nil + return adapter.NewTextResult("StreamNative Cloud context set successfully"), nil } -func handleAvailableContexts(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { - promptResponse, err := HandleListPulsarClusters(ctx, mcp.GetPromptRequest{}) +func handleAvailableContexts(ctx context.Context, _ *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { + promptResponse, err := HandleListPulsarClusters(ctx, &mcpsdk.GetPromptRequest{}) if err != nil || promptResponse == nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list pulsar clusters: %v", err)), nil + return adapter.NewErrorResult("Failed to list pulsar clusters: %v", err), nil } response := "" for _, message := range promptResponse.Messages { - if textContent, ok := message.Content.(mcp.TextContent); ok { + if textContent, ok := message.Content.(*mcpsdk.TextContent); ok { response += textContent.Text + "\n" } } response += "Please confirm the target context cluster with USER if there are multiple clusters!" - return mcp.NewToolResultText(response), nil + return adapter.NewTextResult(response), nil } + +// ContextNotSetErr is returned when the context is not set +var ContextNotSetErr = fmt.Errorf("context not set. Please use sncloud_context_use_cluster to set the context first") diff --git a/pkg/mcp/streamnative_resources_log_tools.go b/pkg/mcp/streamnative_resources_log_tools.go index e3d2a0f..2fa1f31 100644 --- a/pkg/mcp/streamnative_resources_log_tools.go +++ b/pkg/mcp/streamnative_resources_log_tools.go @@ -26,54 +26,55 @@ import ( "strings" "time" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" context2 "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" ) var FunctionConnectorList = []string{"sink", "source", "function", "kafka-connect"} // StreamNativeAddLogTools adds log tools -func StreamNativeAddLogTools(s *server.MCPServer, _ bool, features []string) { +func StreamNativeAddLogTools(s *MCPServer, _ bool, features []string) { if !slices.Contains(features, string(FeatureStreamNativeCloud)) && !slices.Contains(features, string(FeatureAll)) { return } - logTool := mcp.NewTool("sncloud_logs", - mcp.WithDescription("Display the logs of resources in StreamNative Cloud, including pulsar functions, pulsar source connectors, pulsar sink connectors, and kafka connect connectors logs running along with PulsarInstance and PulsarCluster."+ + logTool := builders.NewTool("sncloud_logs", + builders.WithDescription("Display the logs of resources in StreamNative Cloud, including pulsar functions, pulsar source connectors, pulsar sink connectors, and kafka connect connectors logs running along with PulsarInstance and PulsarCluster."+ "This tool is used to help you debug the issues of resources in StreamNative Cloud. You can use `sncloud_context_use_cluster` to change the context to a specific cluster first, then use this tool to get the logs of resources in the cluster. This tool is suggested to be used with 'pulsar_admin_functions', 'pulsar_admin_sinks', 'pulsar_admin_sources', and 'kafka_admin_connect'"), - mcp.WithString("component", mcp.Required(), - mcp.Description("The component to get logs from, including "+strings.Join(FunctionConnectorList, ", ")), - mcp.Enum(FunctionConnectorList...), + builders.WithString("component", builders.Required(), + builders.Description("The component to get logs from, including "+strings.Join(FunctionConnectorList, ", ")), + builders.Enum(FunctionConnectorList...), ), - mcp.WithString("name", mcp.Required(), - mcp.Description("The name of the resource to get logs from."), + builders.WithString("name", builders.Required(), + builders.Description("The name of the resource to get logs from."), ), - mcp.WithString("tenant", mcp.Required(), - mcp.Description("The pulsar tenant of the resource to get logs from. This is required for pulsar functions, pulsar source connectors, pulsar sink connectors. For kafka connect connectors, this is optional, and the default value is 'public'."), - mcp.DefaultString("public"), + builders.WithString("tenant", builders.Required(), + builders.Description("The pulsar tenant of the resource to get logs from. This is required for pulsar functions, pulsar source connectors, pulsar sink connectors. For kafka connect connectors, this is optional, and the default value is 'public'."), + builders.DefaultString("public"), ), - mcp.WithString("namespace", mcp.Required(), - mcp.Description("The pulsar namespace of the resource to get logs from. This is required for pulsar functions, pulsar source connectors, pulsar sink connectors. For kafka connect connectors, this is optional, and the default value is 'default'."), - mcp.DefaultString("default"), + builders.WithString("namespace", builders.Required(), + builders.Description("The pulsar namespace of the resource to get logs from. This is required for pulsar functions, pulsar source connectors, pulsar sink connectors. For kafka connect connectors, this is optional, and the default value is 'default'."), + builders.DefaultString("default"), ), - mcp.WithString("size", - mcp.Description("String format of the number of lines to get from the logs, e.g. 10, 100, etc. (default: 20)"), - mcp.DefaultString("20"), + builders.WithString("size", + builders.Description("String format of the number of lines to get from the logs, e.g. 10, 100, etc. (default: 20)"), + builders.DefaultString("20"), ), - mcp.WithNumber("replica_id", - mcp.Description("The replica index of the resource to get logs from, this is used for multiple replicas of running pulsar functions, pulsar source connectors, pulsar sink connectors, and kafka connect connectors. The value should be a positive integer (like 0, 1, 2, etc.), and the default value is -1, which means all replicas."), - mcp.DefaultNumber(-1), + builders.WithNumber("replica_id", + builders.Description("The replica index of the resource to get logs from, this is used for multiple replicas of running pulsar functions, pulsar source connectors, pulsar sink connectors, and kafka connect connectors. The value should be a positive integer (like 0, 1, 2, etc.), and the default value is -1, which means all replicas."), + builders.DefaultNumber(-1), ), - mcp.WithString("timestamp", - mcp.Description("Start timestamp of logs, for example: 1662430984225"), + builders.WithString("timestamp", + builders.Description("Start timestamp of logs, for example: 1662430984225"), ), - mcp.WithString("since", - mcp.Description("Since time of logs, numbers end with s|m|h, for example one hour ago: 1h"), + builders.WithString("since", + builders.Description("Since time of logs, numbers end with s|m|h, for example one hour ago: 1h"), ), - mcp.WithBoolean("previous_container", - mcp.Description("Return previous terminated container logs, defaults to false."), - mcp.DefaultBool(false), + builders.WithBoolean("previous_container", + builders.Description("Return previous terminated container logs, defaults to false."), + builders.DefaultBool(false), ), ) s.AddTool(logTool, handleStreamNativeResourcesLog) @@ -108,7 +109,7 @@ type LogContent struct { Record int64 `json:"record"` } -func handleStreamNativeResourcesLog(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func handleStreamNativeResourcesLog(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get log client from session session := context2.GetSNCloudSession(ctx) if session == nil { @@ -116,35 +117,35 @@ func handleStreamNativeResourcesLog(ctx context.Context, request mcp.CallToolReq } instance, cluster, organization := session.Ctx.PulsarInstance, session.Ctx.PulsarCluster, session.Ctx.Organization if instance == "" || cluster == "" || organization == "" { - return mcp.NewToolResultError("No context is set, please use `sncloud_context_use_cluster` to set the context first."), nil + return adapter.NewErrorResult("No context is set, please use `sncloud_context_use_cluster` to set the context first."), nil } // Extract required parameters with validation - component, err := request.RequireString("component") + component, err := adapter.RequireString(request, "component") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get component: %v", err)), nil + return adapter.NewErrorResult("Failed to get component: %v", err), nil } - name, err := request.RequireString("name") + name, err := adapter.RequireString(request, "name") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get name: %v", err)), nil + return adapter.NewErrorResult("Failed to get name: %v", err), nil } - tenant := request.GetString("tenant", "public") + tenant := adapter.GetString(request, "tenant", "public") - namespace := request.GetString("namespace", "default") + namespace := adapter.GetString(request, "namespace", "default") - size := request.GetString("size", "20") + size := adapter.GetString(request, "size", "20") - replicaID := request.GetInt("replica_id", -1) + replicaID := adapter.GetInt(request, "replica_id", -1) if replicaID == 0 { replicaID = -1 } - timestampStr := request.GetString("timestamp", "") - sinceStr := request.GetString("since", "") + timestampStr := adapter.GetString(request, "timestamp", "") + sinceStr := adapter.GetString(request, "since", "") - previousContainer := request.GetBool("previous_container", false) + previousContainer := adapter.GetBool(request, "previous_container", false) if sinceStr != "" { sinceStr = "-" + sinceStr @@ -179,20 +180,20 @@ func handleStreamNativeResourcesLog(ctx context.Context, request mcp.CallToolReq client, err := session.GetLogClient() if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get logging client: %v", err)), nil + return adapter.NewErrorResult("Failed to get logging client: %v", err), nil } results := []string{} results, err = logOptions.getLogs(client, 0, 0, results) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get logs: %v", err)), nil + return adapter.NewErrorResult("Failed to get logs: %v", err), nil } if len(results) == 0 { - return mcp.NewToolResultText("No logs found"), nil + return adapter.NewTextResult("No logs found"), nil } - return mcp.NewToolResultText(strings.Join(results, "\n")), nil + return adapter.NewTextResult(strings.Join(results, "\n")), nil } func (o *LogOptions) getLogs(client *http.Client, position int64, diff --git a/pkg/mcp/streamnative_resources_tools.go b/pkg/mcp/streamnative_resources_tools.go index 8798def..81e6e09 100644 --- a/pkg/mcp/streamnative_resources_tools.go +++ b/pkg/mcp/streamnative_resources_tools.go @@ -23,47 +23,48 @@ import ( "slices" "strings" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/streamnative/streamnative-mcp-server/pkg/common" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/builders" + "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/adapter" context2 "github.com/streamnative/streamnative-mcp-server/pkg/mcp/internal/context" sncloud "github.com/streamnative/streamnative-mcp-server/sdk/sdk-apiserver" ) // StreamNativeAddResourceTools adds StreamNative resources tools -func StreamNativeAddResourceTools(s *server.MCPServer, readOnly bool, features []string) { +func StreamNativeAddResourceTools(s *MCPServer, readOnly bool, features []string) { if !slices.Contains(features, string(FeatureStreamNativeCloud)) && !slices.Contains(features, string(FeatureAll)) { return } if !readOnly { // Add Apply tool - applyTool := mcp.NewTool("sncloud_resources_apply", - mcp.WithDescription("Apply StreamNative Cloud resources from JSON definitions. This tool allows you to apply (create or update) StreamNative Cloud resources such as PulsarInstances and PulsarClusters using JSON definitions. Please give feedback to USER if the resource is applied with error, and ask USER to check the resource definition."), - mcp.WithString("json_content", mcp.Required(), - mcp.Description("The JSON content to apply."), + applyTool := builders.NewTool("sncloud_resources_apply", + builders.WithDescription("Apply StreamNative Cloud resources from JSON definitions. This tool allows you to apply (create or update) StreamNative Cloud resources such as PulsarInstances and PulsarClusters using JSON definitions. Please give feedback to USER if the resource is applied with error, and ask USER to check the resource definition."), + builders.WithString("json_content", builders.Required(), + builders.Description("The JSON content to apply."), ), - mcp.WithBoolean("dry_run", - mcp.Description("If true, only validate the resource without applying it to the server."), - mcp.DefaultBool(false), + builders.WithBoolean("dry_run", + builders.Description("If true, only validate the resource without applying it to the server."), + builders.DefaultBool(false), ), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ + builders.WithToolAnnotation(builders.ToolAnnotation{ Title: "Apply StreamNative Cloud Resources", }), ) // Add delete tool - deleteTool := mcp.NewTool("sncloud_resources_delete", - mcp.WithDescription("Delete StreamNative Cloud resources. This tool allows you to delete StreamNative Cloud resources such as PulsarInstances and PulsarClusters."), - mcp.WithString("name", mcp.Required(), - mcp.Description("The name of the resource to delete."), + deleteTool := builders.NewTool("sncloud_resources_delete", + builders.WithDescription("Delete StreamNative Cloud resources. This tool allows you to delete StreamNative Cloud resources such as PulsarInstances and PulsarClusters."), + builders.WithString("name", builders.Required(), + builders.Description("The name of the resource to delete."), ), - mcp.WithString("type", mcp.Required(), - mcp.Description("The type of the resource to delete, it can be PulsarInstance or PulsarCluster."), - mcp.Enum("PulsarInstance", "PulsarCluster"), + builders.WithString("type", builders.Required(), + builders.Description("The type of the resource to delete, it can be PulsarInstance or PulsarCluster."), + builders.Enum("PulsarInstance", "PulsarCluster"), ), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ + builders.WithToolAnnotation(builders.ToolAnnotation{ Title: "Delete StreamNative Cloud Resources", - DestructiveHint: &[]bool{true}[0], + DestructiveHint: func() *bool { b := true; return &b }(), }), ) s.AddTool(applyTool, handleStreamNativeResourcesApply) @@ -86,22 +87,22 @@ type Metadata struct { } // handleStreamNativeResourcesApply handles the streaming_cloud_resources_apply tool -func handleStreamNativeResourcesApply(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func handleStreamNativeResourcesApply(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { // Get necessary parameters snConfig := common.GetOptions(ctx) organization := snConfig.Organization if organization == "" { - return mcp.NewToolResultError("No organization is set. Please set the organization using the appropriate context tool."), nil + return adapter.NewErrorResult("No organization is set. Please set the organization using the appropriate context tool."), nil } // Get YAML content - jsonContent, err := request.RequireString("json_content") + jsonContent, err := adapter.RequireString(request, "json_content") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get json_content: %v", err)), nil + return adapter.NewErrorResult("Failed to get json_content: %v", err), nil } // Get dry_run flag - dryRun := request.GetBool("dry_run", false) + dryRun := adapter.GetBool(request, "dry_run", false) // Get API client from session session := context2.GetSNCloudSession(ctx) @@ -111,24 +112,24 @@ func handleStreamNativeResourcesApply(ctx context.Context, request mcp.CallToolR apiClient, err := session.GetAPIClient() if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get API client: %v", err)), nil + return adapter.NewErrorResult("Failed to get API client: %v", err), nil } jsonContent = strings.TrimSpace(jsonContent) if jsonContent == "" { - return mcp.NewToolResultError("No valid resources found in the provided JSON."), nil + return adapter.NewErrorResult("No valid resources found in the provided JSON."), nil } // Parse YAML document var resource Resource err = json.Unmarshal([]byte(jsonContent), &resource) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to parse JSON document: %v", err)), nil + return adapter.NewErrorResult("Failed to parse JSON document: %v", err), nil } // Check if resource is valid if resource.APIVersion == "" || resource.Kind == "" { - return mcp.NewToolResultError("Invalid resource definition."), nil + return adapter.NewErrorResult("Invalid resource definition."), nil } // Set namespace if not specified @@ -139,10 +140,10 @@ func handleStreamNativeResourcesApply(ctx context.Context, request mcp.CallToolR // Apply resource result, err := applyResource(ctx, apiClient, resource, jsonContent, organization, dryRun) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to apply resource: %v", err)), nil + return adapter.NewErrorResult("Failed to apply resource: %v", err), nil } - return mcp.NewToolResultText(result), nil + return adapter.NewTextResult(result), nil } // applyResource applies the resource based on its type @@ -339,18 +340,18 @@ func applyPulsarCluster(ctx context.Context, apiClient *sncloud.APIClient, jsonC } // handleStreamNativeResourcesDelete handles the streaming_cloud_resources_delete tool -func handleStreamNativeResourcesDelete(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func handleStreamNativeResourcesDelete(ctx context.Context, request *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) { snConfig := common.GetOptions(ctx) organization := snConfig.Organization - name, err := request.RequireString("name") + name, err := adapter.RequireString(request, "name") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get name: %v", err)), nil + return adapter.NewErrorResult("Failed to get name: %v", err), nil } - resourceType, err := request.RequireString("type") + resourceType, err := adapter.RequireString(request, "type") if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get type: %v", err)), nil + return adapter.NewErrorResult("Failed to get type: %v", err), nil } // Get API client from session @@ -361,7 +362,7 @@ func handleStreamNativeResourcesDelete(ctx context.Context, request mcp.CallTool apiClient, err := session.GetAPIClient() if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get API client: %v", err)), nil + return adapter.NewErrorResult("Failed to get API client: %v", err), nil } switch resourceType { @@ -372,13 +373,13 @@ func handleStreamNativeResourcesDelete(ctx context.Context, request mcp.CallTool //nolint:bodyclose _, _, err = apiClient.CloudStreamnativeIoV1alpha1Api.DeleteCloudStreamnativeIoV1alpha1NamespacedPulsarCluster(ctx, name, organization).Execute() default: - return mcp.NewToolResultError(fmt.Sprintf("Unsupported resource type: %s", resourceType)), nil + return adapter.NewErrorResult("Unsupported resource type: %s", resourceType), nil } // the delete operation will return a V1Status object, which is not handled by the SDK if err != nil && !strings.Contains(err.Error(), "json: cannot unmarshal") { - return mcp.NewToolResultError(fmt.Sprintf("failed to delete resource: %v", err)), nil + return adapter.NewErrorResult("failed to delete resource: %v", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("Resource %q %s deleted", name, resourceType)), nil + return adapter.NewTextResult(fmt.Sprintf("Resource %q %s deleted", name, resourceType)), nil } diff --git a/pkg/schema/avro.go b/pkg/schema/avro.go index 5ab6530..e1073ee 100644 --- a/pkg/schema/avro.go +++ b/pkg/schema/avro.go @@ -16,10 +16,9 @@ package schema import ( "fmt" - // "reflect" // No longer needed here + "github.com/invopop/jsonschema" cliutils "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" ) type AvroConverter struct { @@ -30,7 +29,7 @@ func NewAvroConverter() *AvroConverter { return &AvroConverter{} } -func (c *AvroConverter) ToMCPToolInputSchemaProperties(schemaInfo *cliutils.SchemaInfo) ([]mcp.ToolOption, error) { +func (c *AvroConverter) ToMCPToolInputSchemaProperties(schemaInfo *cliutils.SchemaInfo) (*jsonschema.Schema, error) { if schemaInfo.Type != "AVRO" { return nil, fmt.Errorf("expected AVRO schema, got %s", schemaInfo.Type) } diff --git a/pkg/schema/avro_core.go b/pkg/schema/avro_core.go index be84a53..70f548e 100644 --- a/pkg/schema/avro_core.go +++ b/pkg/schema/avro_core.go @@ -21,12 +21,12 @@ import ( "strings" "github.com/hamba/avro/v2" - "github.com/mark3labs/mcp-go/mcp" + "github.com/invopop/jsonschema" ) // processAvroSchemaStringToMCPToolInput takes an AVRO schema string, parses it, -// and converts it to MCP tool input schema properties. -func processAvroSchemaStringToMCPToolInput(avroSchemaString string) ([]mcp.ToolOption, error) { +// and converts it to jsonschema.Schema for tool input. +func processAvroSchemaStringToMCPToolInput(avroSchemaString string) (*jsonschema.Schema, error) { schema, err := avro.Parse(avroSchemaString) if err != nil { return nil, fmt.Errorf("failed to parse AVRO schema: %w", err) @@ -34,24 +34,36 @@ func processAvroSchemaStringToMCPToolInput(avroSchemaString string) ([]mcp.ToolO recordSchema, ok := schema.(*avro.RecordSchema) if !ok { - // If it's not a record, perhaps it's a simpler type that can't be directly mapped to tool options, - // or we need a different handling strategy. For now, strict record check. return nil, fmt.Errorf("expected AVRO record schema at the top level, got %s", reflect.TypeOf(schema).String()) } - var opts []mcp.ToolOption + props := jsonschema.NewProperties() + required := make([]string, 0) + for _, field := range recordSchema.Fields() { - fieldOption, err := avroFieldToMcpOption(field) + fieldSchema, isRequired, err := avroFieldToJsonSchema(field) if err != nil { return nil, fmt.Errorf("failed to convert field '%s': %w", field.Name(), err) } - opts = append(opts, fieldOption) + props.Set(field.Name(), fieldSchema) + if isRequired { + required = append(required, field.Name()) + } } - return opts, nil + + result := &jsonschema.Schema{ + Type: "object", + Properties: props, + } + if len(required) > 0 { + result.Required = required + } + + return result, nil } -// avroFieldToMcpOption converts a single AVRO field to an mcp.ToolOption. -func avroFieldToMcpOption(field *avro.Field) (mcp.ToolOption, error) { +// avroFieldToJsonSchema converts a single AVRO field to jsonschema.Schema. +func avroFieldToJsonSchema(field *avro.Field) (*jsonschema.Schema, bool, error) { fieldType := field.Type() fieldName := field.Name() @@ -59,11 +71,11 @@ func avroFieldToMcpOption(field *avro.Field) (mcp.ToolOption, error) { if field.Doc() != "" { description = field.Doc() } else { - description = fmt.Sprintf("%s (type: %s)", fieldName, strings.ReplaceAll(fieldType.String(), "\"", "")) // Default description + description = fmt.Sprintf("%s (type: %s)", fieldName, strings.ReplaceAll(fieldType.String(), "\"", "")) } isRequired := true - var underlyingTypeForDefault avro.Schema = fieldType // Used to check default value against non-union type + var underlyingTypeForDefault avro.Schema = fieldType if unionSchema, ok := fieldType.(*avro.UnionSchema); ok { isNullAble := false @@ -77,182 +89,144 @@ func avroFieldToMcpOption(field *avro.Field) (mcp.ToolOption, error) { } isRequired = !isNullAble - // If it's a nullable union with one other type (e.g., ["null", "string"]), - // treat it as that other type for default value and MCP type mapping. - //nolint:gocritic // This is a valid use of len(nonNullTypes) == 1 if isNullAble && len(nonNullTypes) == 1 { underlyingTypeForDefault = nonNullTypes[0] } else if len(nonNullTypes) == 1 { - // Not nullable, but still a union with one type (should ideally not happen, but handle) underlyingTypeForDefault = nonNullTypes[0] } else if len(nonNullTypes) > 1 { - // Complex union (e.g., ["string", "int"]), for MCP, describe as string and mention union nature. - // Default values for complex unions are tricky with current MCP options. - // MCP schema might need to be a string with a description of the union. - props := []mcp.PropertyOption{mcp.Description(description + " (union type: " + strings.ReplaceAll(fieldType.String(), "\"", "") + ")")} - if isRequired { - props = append(props, mcp.Required()) - } - // Default value for complex union is not straightforward to map to a single MCP type's default. - // We will skip setting mcp.Default... for complex unions for now. - return mcp.WithString(fieldName, props...), nil - } - // If only "null" type was in union, or empty nonNullTypes (invalid schema), this will be caught by later type switch. + // Complex union - represent as string with description + schema := &jsonschema.Schema{ + Type: "string", + Description: description + " (union type: " + strings.ReplaceAll(fieldType.String(), "\"", "") + ")", + } + return schema, isRequired, nil + } } - var prop []mcp.PropertyOption - if isRequired { - prop = append(prop, mcp.Required()) + schema := &jsonschema.Schema{ + Description: description, } - prop = append(prop, mcp.Description(description)) - var opt mcp.ToolOption - - // Use underlyingTypeForDefault for determining MCP type and handling default values - // This handles cases like ["null", "string"] by treating it as "string" for MCP mapping. + // Use underlyingTypeForDefault for determining type and handling default values effectiveType := underlyingTypeForDefault.Type() switch effectiveType { case avro.String: + schema.Type = "string" if field.HasDefault() { if defaultVal, ok := field.Default().(string); ok { - prop = append(prop, mcp.DefaultString(defaultVal)) + schema.Default = defaultVal } } - opt = mcp.WithString(fieldName, prop...) - case avro.Int, avro.Long: // MCP 'number' can represent both + case avro.Int, avro.Long: + schema.Type = "number" if field.HasDefault() { - // Avro library parses numeric defaults as float64 for int/long/float/double from JSON representation if defaultVal, ok := field.Default().(float64); ok { - prop = append(prop, mcp.DefaultNumber(defaultVal)) - } else if defaultIntVal, ok := field.Default().(int); ok { // direct int - prop = append(prop, mcp.DefaultNumber(float64(defaultIntVal))) + schema.Default = defaultVal + } else if defaultIntVal, ok := field.Default().(int); ok { + schema.Default = float64(defaultIntVal) } else if defaultInt32Val, ok := field.Default().(int32); ok { - prop = append(prop, mcp.DefaultNumber(float64(defaultInt32Val))) + schema.Default = float64(defaultInt32Val) } else if defaultInt64Val, ok := field.Default().(int64); ok { - prop = append(prop, mcp.DefaultNumber(float64(defaultInt64Val))) + schema.Default = float64(defaultInt64Val) } } - opt = mcp.WithNumber(fieldName, prop...) - case avro.Float, avro.Double: // MCP 'number' can represent both + case avro.Float, avro.Double: + schema.Type = "number" if field.HasDefault() { if defaultVal, ok := field.Default().(float64); ok { - prop = append(prop, mcp.DefaultNumber(defaultVal)) + schema.Default = defaultVal } } - opt = mcp.WithNumber(fieldName, prop...) case avro.Boolean: + schema.Type = "boolean" if field.HasDefault() { if defaultVal, ok := field.Default().(bool); ok { - prop = append(prop, mcp.DefaultBool(defaultVal)) + schema.Default = defaultVal } } - opt = mcp.WithBoolean(fieldName, prop...) case avro.Bytes, avro.Fixed: + schema.Type = "string" if field.HasDefault() { if defaultVal, ok := field.Default().(string); ok { - prop = append(prop, mcp.DefaultString(defaultVal)) + schema.Default = defaultVal } else if defaultBytes, ok := field.Default().([]byte); ok { - prop = append(prop, mcp.DefaultString(string(defaultBytes))) // Or base64 + schema.Default = string(defaultBytes) } } - // For Fixed type, add size information to description if fixedSchema, ok := underlyingTypeForDefault.(*avro.FixedSchema); ok { - description += fmt.Sprintf(" (fixed size: %d bytes)", fixedSchema.Size()) - prop[0] = mcp.Description(description) // Update description in prop + schema.Description = fmt.Sprintf("%s (fixed size: %d bytes)", description, fixedSchema.Size()) } - opt = mcp.WithString(fieldName, prop...) // Always use WithString for Bytes/Fixed in MCP options case avro.Array: - arraySchema, _ := underlyingTypeForDefault.(*avro.ArraySchema) // Safe due to switch - itemsDef, err := avroSchemaDefinitionToMcpProperties("item", arraySchema.Items(), "Array items") + arraySchema, _ := underlyingTypeForDefault.(*avro.ArraySchema) + itemsSchema, err := avroSchemaDefinitionToJsonSchema(arraySchema.Items(), "Array items") if err != nil { - return nil, fmt.Errorf("failed to convert array items for field '%s': %w", fieldName, err) + return nil, false, fmt.Errorf("failed to convert array items for field '%s': %w", fieldName, err) } - prop = append(prop, mcp.Items(itemsDef)) - // Default for array is not directly supported by mcp.DefaultArray like mcp.DefaultString etc. - // It would require converting []any to a string representation or specific handling. - opt = mcp.WithArray(fieldName, prop...) + schema.Type = "array" + schema.Items = itemsSchema case avro.Map: - mapSchema, _ := underlyingTypeForDefault.(*avro.MapSchema) // Safe due to switch - // For MCP, map values are usually represented as an object where keys are arbitrary strings - // and all values conform to a single schema. - // mcp.Properties expects a map[string]any defining named properties. - // This is a slight mismatch. MCP's WithObject with mcp.Properties(map[string]any{"*": valuesDef}) - // is one way, or a more flexible mcp.WithMap that takes a value schema. - // For now, let's assume map values translate to a generic object property definition. - valuesDef, err := avroSchemaDefinitionToMcpProperties("value", mapSchema.Values(), "Map values") + mapSchema, _ := underlyingTypeForDefault.(*avro.MapSchema) + valuesSchema, err := avroSchemaDefinitionToJsonSchema(mapSchema.Values(), "Map values") if err != nil { - return nil, fmt.Errorf("failed to convert map values for field '%s': %w", fieldName, err) - } - // This isn't a perfect fit for mcp.Properties which expects fixed keys. - // A better MCP representation for a map might be WithObject where AdditionalProperties is set. - // For now, we describe it as an object and the value schema applies to all its properties. - // A more accurate MCP representation might be needed if maps are used extensively. - // Let's use a single property "*" to denote the schema for all values. - prop = append(prop, mcp.Properties(map[string]any{"*": valuesDef})) - opt = mcp.WithObject(fieldName, prop...) // Representing map as a generic object. + return nil, false, fmt.Errorf("failed to convert map values for field '%s': %w", fieldName, err) + } + schema.Type = "object" + schema.AdditionalProperties = valuesSchema case avro.Record: - recordSchema, _ := underlyingTypeForDefault.(*avro.RecordSchema) // Safe due to switch - subProps := make(map[string]any) + recordSchema, _ := underlyingTypeForDefault.(*avro.RecordSchema) + subProps := jsonschema.NewProperties() + subRequired := make([]string, 0) for _, subField := range recordSchema.Fields() { - // Recursively call avroFieldToMcpOption to get the ToolOption, then extract its schema part? - // No, avroSchemaDefinitionToMcpProperties is for defining schema of items/values, not named fields. - // We need to build the properties map for mcp.WithObject. - // Each subField needs its schema definition. - var subFieldDescription string - if subField.Doc() != "" { - subFieldDescription = subField.Doc() - } else { - subFieldDescription = fmt.Sprintf("%s (type: %s)", subField.Name(), strings.ReplaceAll(subField.Type().String(), "\"", "")) // Default description - } - subFieldDef, err := avroSchemaDefinitionToMcpProperties(subField.Name(), subField.Type(), subFieldDescription) + subFieldSchema, subIsRequired, err := avroFieldToJsonSchema(subField) if err != nil { - return nil, fmt.Errorf("failed to convert sub-field '%s' of record '%s': %w", subField.Name(), fieldName, err) + return nil, false, fmt.Errorf("failed to convert sub-field '%s' of record '%s': %w", subField.Name(), fieldName, err) + } + subProps.Set(subField.Name(), subFieldSchema) + if subIsRequired { + subRequired = append(subRequired, subField.Name()) } - subProps[subField.Name()] = subFieldDef } - prop = append(prop, mcp.Properties(subProps)) - opt = mcp.WithObject(fieldName, prop...) + schema.Type = "object" + schema.Properties = subProps + if len(subRequired) > 0 { + schema.Required = subRequired + } case avro.Enum: - enumSchema, _ := underlyingTypeForDefault.(*avro.EnumSchema) // Safe due to switch - prop = append(prop, mcp.Enum(enumSchema.Symbols()...)) + enumSchema, _ := underlyingTypeForDefault.(*avro.EnumSchema) + schema.Type = "string" + schema.Enum = make([]interface{}, len(enumSchema.Symbols())) + for i, symbol := range enumSchema.Symbols() { + schema.Enum[i] = symbol + } if field.HasDefault() { - if defaultVal, ok := field.Default().(string); ok { // Enum default is string - prop = append(prop, mcp.DefaultString(defaultVal)) + if defaultVal, ok := field.Default().(string); ok { + schema.Default = defaultVal } } - opt = mcp.WithString(fieldName, prop...) // Enum is represented as string in MCP case avro.Null: - // This case should ideally be handled by the union logic making the field not required. - // If a field is solely "null", it's an odd schema. For MCP, maybe a string with note. - // If isRequired is true here (meaning it wasn't a union with null), it's a non-optional null field. - // This is unusual. Let's represent as a non-required string. - if isRequired { // Should not happen if only type is null and handled by union logic - prop = []mcp.PropertyOption{mcp.Description(description + " (null type)")} // remove mcp.Required - } else { - prop = append(prop, mcp.Description(description+" (null type)")) - } - opt = mcp.WithString(fieldName, prop...) // Or handle as a special ignorable field? - default: - // For unknown or unsupported AVRO types, represent as a string in MCP with a description. - var defaultCaseProps []mcp.PropertyOption - defaultCaseProps = append(defaultCaseProps, mcp.Description(description+" (unsupported AVRO type: "+string(effectiveType)+")")) + schema.Type = "string" + schema.Description = description + " (null type)" if isRequired { - defaultCaseProps = append(defaultCaseProps, mcp.Required()) + isRequired = false } - opt = mcp.WithString(fieldName, defaultCaseProps...) + default: + schema.Type = "string" + schema.Description = description + " (unsupported AVRO type: " + string(effectiveType) + ")" } - return opt, nil + + return schema, isRequired, nil } -// avroSchemaDefinitionToMcpProperties converts an AVRO schema definition (like for array items or map values) -// into a map[string]any structure suitable for mcp.PropertyOption's Items or Properties. -func avroSchemaDefinitionToMcpProperties(name string, avroDef avro.Schema, description string) (map[string]any, error) { - props := make(map[string]any) +// avroSchemaDefinitionToJsonSchema converts an AVRO schema definition (like for array items or map values) +// into jsonschema.Schema. +func avroSchemaDefinitionToJsonSchema(avroDef avro.Schema, description string) (*jsonschema.Schema, error) { + schema := &jsonschema.Schema{ + Description: description, + } + if description == "" { - props["description"] = fmt.Sprintf("Schema for %s", name) - } else { - props["description"] = description + schema.Description = fmt.Sprintf("Schema for type") } // Handle unions for nested types as well @@ -264,87 +238,82 @@ func avroSchemaDefinitionToMcpProperties(name string, avroDef avro.Schema, descr nonNullTypes = append(nonNullTypes, t) } } - //nolint:gocritic // This is a valid use of len(nonNullTypes) == 1 if len(nonNullTypes) == 1 { effectiveSchema = nonNullTypes[0] - props["description"] = props["description"].(string) + " (nullable)" + schema.Description = schema.Description + " (nullable)" } else if len(nonNullTypes) > 1 { - props["type"] = "string" // Represent complex union as string - props["description"] = props["description"].(string) + " (union type: " + strings.ReplaceAll(avroDef.String(), "\"", "") + ")" - return props, nil - } else { // Only null in union or empty union (invalid) - props["type"] = "string" // Fallback for null type - props["description"] = props["description"].(string) + " (effectively null type)" - return props, nil + schema.Type = "string" + schema.Description = schema.Description + " (union type: " + strings.ReplaceAll(avroDef.String(), "\"", "") + ")" + return schema, nil + } else { + schema.Type = "string" + schema.Description = schema.Description + " (effectively null type)" + return schema, nil } } switch effectiveSchema.Type() { case avro.String: - props["type"] = "string" + schema.Type = "string" case avro.Int, avro.Long: - props["type"] = "number" + schema.Type = "number" case avro.Float, avro.Double: - props["type"] = "number" + schema.Type = "number" case avro.Boolean: - props["type"] = "boolean" - case avro.Bytes, avro.Fixed: // Fixed size bytes - props["type"] = "string" // Bytes/Fixed represented as string in MCP JSON schema + schema.Type = "boolean" + case avro.Bytes, avro.Fixed: + schema.Type = "string" case avro.Array: arraySchema, _ := effectiveSchema.(*avro.ArraySchema) - itemsProps, err := avroSchemaDefinitionToMcpProperties("item", arraySchema.Items(), "Array items") + itemsSchema, err := avroSchemaDefinitionToJsonSchema(arraySchema.Items(), "Array items") if err != nil { return nil, err } - props["type"] = "array" - props["items"] = itemsProps + schema.Type = "array" + schema.Items = itemsSchema case avro.Map: mapSchema, _ := effectiveSchema.(*avro.MapSchema) - // MCP object properties are named. Avro map keys are strings, values are of a single schema. - // We represent this as an object where all properties conform to the map's value schema. - // The key "*" can signify this pattern. - valuesProps, err := avroSchemaDefinitionToMcpProperties("value", mapSchema.Values(), "Map values schema") + valuesSchema, err := avroSchemaDefinitionToJsonSchema(mapSchema.Values(), "Map values schema") if err != nil { return nil, err } - props["type"] = "object" - // To represent an Avro map (string keys, common value schema) in JSON schema properties: - // we can use "additionalProperties" with the schema of map values. - // Or, for mcp.Properties, we might define a placeholder like "*". - // For now, let's return a structure that indicates it's an object, - // and the `valuesProps` describes the schema for any property within this object. - // This is a common pattern for map-like structures in JSON Schema if not using additionalProperties. - props["properties"] = map[string]any{"*": valuesProps} // Indicating all values have this schema. + schema.Type = "object" + schema.AdditionalProperties = valuesSchema case avro.Record: recordSchema, _ := effectiveSchema.(*avro.RecordSchema) - subProps := make(map[string]any) + subProps := jsonschema.NewProperties() + subRequired := make([]string, 0) for _, field := range recordSchema.Fields() { - var fieldDescription string - if field.Doc() != "" { - fieldDescription = field.Doc() - } else { - fieldDescription = fmt.Sprintf("%s (type: %s)", field.Name(), strings.ReplaceAll(field.Type().String(), "\"", "")) // Default description - } - fieldProp, err := avroSchemaDefinitionToMcpProperties(field.Name(), field.Type(), fieldDescription) + fieldSchema, fieldRequired, err := avroFieldToJsonSchema(field) if err != nil { return nil, err } - subProps[field.Name()] = fieldProp + subProps.Set(field.Name(), fieldSchema) + if fieldRequired { + subRequired = append(subRequired, field.Name()) + } + } + schema.Type = "object" + schema.Properties = subProps + if len(subRequired) > 0 { + schema.Required = subRequired } - props["type"] = "object" - props["properties"] = subProps case avro.Enum: enumSchema, _ := effectiveSchema.(*avro.EnumSchema) - props["type"] = "string" - props["enum"] = enumSchema.Symbols() - case avro.Null: // Should be handled by union logic primarily - props["type"] = "string" // Fallback for a standalone null type. - props["description"] = props["description"].(string) + " (null type)" + schema.Type = "string" + schema.Enum = make([]interface{}, len(enumSchema.Symbols())) + for i, symbol := range enumSchema.Symbols() { + schema.Enum[i] = symbol + } + case avro.Null: + schema.Type = "string" + schema.Description = schema.Description + " (null type)" default: - props["type"] = "string" // Fallback for unknown types - props["description"] = props["description"].(string) + " (unknown AVRO type: " + string(effectiveSchema.Type()) + ")" + schema.Type = "string" + schema.Description = schema.Description + " (unknown AVRO type: " + string(effectiveSchema.Type()) + ")" } - return props, nil + + return schema, nil } // validateArgumentsAgainstAvroSchemaString validates arguments against an AVRO schema string. @@ -354,23 +323,15 @@ func validateArgumentsAgainstAvroSchemaString(arguments map[string]any, avroSche return fmt.Errorf("failed to parse AVRO schema for validation: %w", err) } - // Expecting a record schema at the top level for arguments map recordSchema, ok := schema.(*avro.RecordSchema) if !ok { - // If the schema is not a record, but arguments are a map, it's a mismatch unless the schema is a map itself. - // However, tool inputs are typically records/objects. - // If schema is a single type (e.g. string), arguments shouldn't be a map. This needs clarification on Pulsar schema use. - // For now, assume top-level schema for arguments is a record. return fmt.Errorf("expected AVRO record schema for validating arguments map, got %s", reflect.TypeOf(schema).String()) } // Check for missing required fields for _, field := range recordSchema.Fields() { fieldName := field.Name() - // Determine if the field is effectively required for validation purposes - // (not nullable in a union, or a non-union type without a default that makes it implicitly optional) - // This `isReq` is used to check if a field *must* be present in the arguments map if it *doesn't* have a default. - isReq := true // Assume required unless part of a nullable union + isReq := true if unionSchemaVal, ok := field.Type().(*avro.UnionSchema); ok { isNullableInUnion := false for _, t := range unionSchemaVal.Types() { @@ -382,30 +343,21 @@ func validateArgumentsAgainstAvroSchemaString(arguments map[string]any, avroSche isReq = !isNullableInUnion } - // Check if the field is present in the arguments map value, valueOk := arguments[fieldName] - // If field is not in arguments map if !valueOk { - // If it's considered required (isReq is true) AND it does not have a default value, - // then it's an error for it to be missing from arguments. if isReq && !field.HasDefault() { return fmt.Errorf("required field '%s' is missing and has no default value", fieldName) } - // If not required (isReq is false), or if it has a default value, it's okay for it to be missing. - // The Avro library itself will handle applying the default during actual serialization/deserialization. - // Our validator's job here is primarily to ensure that if values ARE provided, they are correct, - // and that truly mandatory fields (required and no default) are present. - continue // Move to the next field in the schema + continue } - // If field is present in arguments, validate its value against its schema type if err := validateValueAgainstAvroType(value, field.Type(), fieldName); err != nil { return err } } - // After validating all fields defined in the schema, check for any extra fields in the arguments. + // Check for extra fields for argName := range arguments { foundInSchema := false for _, field := range recordSchema.Fields() { @@ -423,45 +375,39 @@ func validateArgumentsAgainstAvroSchemaString(arguments map[string]any, avroSche } // validateValueAgainstAvroType validates a single value against a given AVRO schema type. -// path is for constructing helpful error messages. func validateValueAgainstAvroType(value any, avroDef avro.Schema, path string) error { if value == nil { - // If value is nil, check if avroDef allows null if avroDef.Type() == avro.Null { - return nil // Explicitly null type allows nil + return nil } if unionSchema, ok := avroDef.(*avro.UnionSchema); ok { for _, t := range unionSchema.Types() { if t.Type() == avro.Null { - return nil // Union includes null type + return nil } } } return fmt.Errorf("field '%s' is null, but schema type '%s' does not permit null", path, avroDef.Type()) } - // If avroDef is a union, try to validate against each type in the union. if unionSchema, ok := avroDef.(*avro.UnionSchema); ok { var lastErr error for _, schemaTypeInUnion := range unionSchema.Types() { - // Skip null type here as we've handled nil value above. If value is not nil, null type won't match. if schemaTypeInUnion.Type() == avro.Null { continue } err := validateValueAgainstAvroType(value, schemaTypeInUnion, path) if err == nil { - return nil // Valid against one of the types in the union + return nil } - lastErr = err // Keep the last error for context if none match + lastErr = err } if lastErr != nil { return fmt.Errorf("field '%s' (value: %v, type: %T) does not match any type in union schema '%s': last error: %w", path, value, value, unionSchema.String(), lastErr) } - // If union was only ["null"] and value is not nil, this will be an error. - return fmt.Errorf("field '%s' (value: %v) of type %T does not match union schema '%s' (no non-null types matched or union is only null)", path, value, value, unionSchema.String()) + return fmt.Errorf("field '%s' (value: %v) of type %T does not match union schema '%s'", path, value, value, unionSchema.String()) } - // Non-union type validation switch avroDef.Type() { case avro.String: if _, ok := value.(string); !ok { @@ -486,9 +432,6 @@ func validateValueAgainstAvroType(value any, avroDef avro.Schema, path string) e if fVal, ok := value.(float64); ok && fVal != float64(int64(fVal)) { return fmt.Errorf("field '%s': expected long, got float64 with fractional part (value: %v)", path, value) } - if fVal, ok := value.(float32); ok && fVal != float32(int64(fVal)) { // float32 to int64 comparison can be tricky with precision - return fmt.Errorf("field '%s': expected long, got float32 with fractional part (value: %v)", path, value) - } return nil default: return fmt.Errorf("field '%s': expected long, got %T (value: %v)", path, value, value) @@ -511,27 +454,23 @@ func validateValueAgainstAvroType(value any, avroDef avro.Schema, path string) e if _, ok := value.(bool); !ok { return fmt.Errorf("field '%s': expected boolean, got %T (value: %v)", path, value, value) } - case avro.Bytes: if _, okStr := value.(string); okStr { - return nil // Allow string for bytes/fixed as per previous logic + return nil } if _, okBytes := value.([]byte); okBytes { - return nil // Also allow []byte directly + return nil } return fmt.Errorf("field '%s': expected string or []byte for bytes, got %T (value: %v)", path, value, value) case avro.Fixed: if _, ok := value.(uint64); ok { - return nil // Allow uint64 for fixed as per previous logic + return nil } return fmt.Errorf("field '%s': expected uint64 for fixed, got %T (value: %v)", path, value, value) case avro.Array: arrSchema, _ := avroDef.(*avro.ArraySchema) - sliceVal, ok := value.([]any) // JSON unmarshals to []any + sliceVal, ok := value.([]any) if !ok { - // Check if it's a typed slice, e.g. []string, []map[string]any, etc. - // This requires more reflection if we want to support e.g. []string directly. - // For map[string]any from JSON, []any is standard. return fmt.Errorf("field '%s': expected array (slice of any), got %T (value: %v)", path, value, value) } for i, item := range sliceVal { @@ -541,7 +480,7 @@ func validateValueAgainstAvroType(value any, avroDef avro.Schema, path string) e } case avro.Map: mapSchema, _ := avroDef.(*avro.MapSchema) - mapVal, ok := value.(map[string]any) // JSON unmarshals to map[string]any + mapVal, ok := value.(map[string]any) if !ok { return fmt.Errorf("field '%s': expected map (map[string]any), got %T (value: %v)", path, value, value) } @@ -552,11 +491,10 @@ func validateValueAgainstAvroType(value any, avroDef avro.Schema, path string) e } case avro.Record: recSchema, _ := avroDef.(*avro.RecordSchema) - mapVal, ok := value.(map[string]any) // JSON unmarshals to map[string]any + mapVal, ok := value.(map[string]any) if !ok { return fmt.Errorf("field '%s': expected object (map[string]any) for record, got %T (value: %v)", path, value, value) } - // Check required fields within the record for _, f := range recSchema.Fields() { isFieldRequired := true if unionF, okF := f.Type().(*avro.UnionSchema); okF { @@ -575,7 +513,6 @@ func validateValueAgainstAvroType(value any, avroDef avro.Schema, path string) e return fmt.Errorf("field '%s.%s' is required but missing", path, f.Name()) } } - // Validate present fields for k, v := range mapVal { var recField *avro.Field for _, f := range recSchema.Fields() { @@ -608,55 +545,34 @@ func validateValueAgainstAvroType(value any, avroDef avro.Schema, path string) e return fmt.Errorf("field '%s': value '%s' is not a valid symbol for enum %s. Valid symbols: %v", path, strVal, enumSchema.FullName(), enumSchema.Symbols()) } case avro.Null: - if value == nil { - // If value is nil, check if avroDef allows null - if avroDef.Type() == avro.Null { - return nil // Explicitly null type allows nil - } - if unionSchema, ok := avroDef.(*avro.UnionSchema); ok { - for _, t := range unionSchema.Types() { - if t.Type() == avro.Null { - return nil // Union includes null type - } - } - } - return fmt.Errorf("field '%s' is null, but schema type '%s' does not permit null", path, avroDef.Type()) + if value != nil { + return fmt.Errorf("field '%s': schema type is explicitly 'null' but received non-nil value %T (value: %v)", path, value, value) } - // If value is not nil, it's an error. Nil value handled at the start of the function. - // This means value is non-nil here. - return fmt.Errorf("field '%s': schema type is explicitly 'null' but received non-nil value %T (value: %v)", path, value, value) - default: return fmt.Errorf("field '%s': unsupported AVRO type '%s' for validation", path, avroDef.Type()) } - return nil // Should be unreachable if all cases are handled or return, but as a fallback + return nil } // serializeArgumentsToAvroBinary validates arguments against an AVRO schema string // and then serializes them to AVRO binary format. func serializeArgumentsToAvroBinary(arguments map[string]any, avroSchemaString string) ([]byte, error) { - // First, validate arguments. - // The validation logic already parses the schema string. if err := validateArgumentsAgainstAvroSchemaString(arguments, avroSchemaString); err != nil { return nil, fmt.Errorf("arguments validation failed before AVRO serialization: %w", err) } - // Parse schema again for marshaling (or pass parsed schema from validation if we refactor to return it) schema, err := avro.Parse(avroSchemaString) if err != nil { - // This error should ideally not happen if validation passed, as it also parses. - return nil, fmt.Errorf("failed to parse AVRO schema for serialization (should have been caught by validation): %w", err) + return nil, fmt.Errorf("failed to parse AVRO schema for serialization: %w", err) } - // Before marshalling, we might need to coerce some types, e.g., string to []byte for "bytes" type if convention is base64 string. coercedArgs := make(map[string]any, len(arguments)) for k, v := range arguments { - coercedArgs[k] = v // Copy existing + coercedArgs[k] = v } recordSchema, ok := schema.(*avro.RecordSchema) if !ok { - // This should ideally not happen if validation passed, but as a safeguard: return nil, fmt.Errorf("parsed schema is not a record schema, cannot prepare arguments for serialization") } @@ -664,15 +580,11 @@ func serializeArgumentsToAvroBinary(arguments map[string]any, avroSchemaString s fieldName := field.Name() val, argExists := arguments[fieldName] if !argExists { - continue // If arg doesn't exist, skip (defaults or optional handled by avro lib or previous validation) + continue } - fieldType := field.Type().Type() // Get the base type, handles unions by checking underlying + fieldType := field.Type().Type() if unionSchema, isUnion := field.Type().(*avro.UnionSchema); isUnion { - // If it's a union, we need to find the actual non-null type for coercion logic - // This part can be complex if multiple non-null types are in union with bytes/fixed. - // For simplicity, assuming if 'bytes' or 'fixed' is a possibility, we check for string coercion. - // A more robust solution would inspect the actual type of 'val' against union possibilities. for _, unionMemberType := range unionSchema.Types() { if unionMemberType.Type() == avro.Bytes || unionMemberType.Type() == avro.Fixed { fieldType = unionMemberType.Type() @@ -683,7 +595,6 @@ func serializeArgumentsToAvroBinary(arguments map[string]any, avroSchemaString s if fieldType == avro.Bytes { if strVal, isStr := val.(string); isStr { - // Attempt to decode if it's a string, assuming base64 for bytes encoded as string decodedBytes, err := base64.StdEncoding.DecodeString(strVal) if err == nil { coercedArgs[fieldName] = decodedBytes @@ -693,8 +604,7 @@ func serializeArgumentsToAvroBinary(arguments map[string]any, avroSchemaString s } } else if fieldType == avro.Fixed { if strVal, isStr := val.(string); isStr { - // For fixed, if it's a string, it must be base64 decodable to the correct length array - fixedSchema, _ := field.Type().(*avro.FixedSchema) // Or resolve from union if necessary + fixedSchema, _ := field.Type().(*avro.FixedSchema) if actualUnionFieldSchema, okUnion := field.Type().(*avro.UnionSchema); okUnion { for _, ut := range actualUnionFieldSchema.Types() { if fs, okUFS := ut.(*avro.FixedSchema); okUFS { @@ -707,18 +617,15 @@ func serializeArgumentsToAvroBinary(arguments map[string]any, avroSchemaString s decodedBytes, err := base64.StdEncoding.DecodeString(strVal) if err == nil { if len(decodedBytes) == fixedSchema.Size() { - // Convert []byte to [N]byte array for fixed type fixedArray := reflect.New(reflect.ArrayOf(fixedSchema.Size(), reflect.TypeOf(byte(0)))).Elem() reflect.Copy(fixedArray, reflect.ValueOf(decodedBytes)) coercedArgs[fieldName] = fixedArray.Interface() } else { - // Length mismatch after decoding return nil, fmt.Errorf("field '%s' (fixed[%d]): base64 decoded string has length %d, expected %d", fieldName, fixedSchema.Size(), len(decodedBytes), fixedSchema.Size()) } - } // else: base64 decoding error, let avro.Marshal handle or error out + } } } else if byteSlice, isSlice := val.([]byte); isSlice { - // If it's already a []byte, check if it's for a Fixed type and needs conversion to [N]byte fixedSchema, _ := field.Type().(*avro.FixedSchema) if actualUnionFieldSchema, okUnion := field.Type().(*avro.UnionSchema); okUnion { for _, ut := range actualUnionFieldSchema.Types() { @@ -734,12 +641,10 @@ func serializeArgumentsToAvroBinary(arguments map[string]any, avroSchemaString s coercedArgs[fieldName] = fixedArray.Interface() } else if fixedSchema != nil && len(byteSlice) != fixedSchema.Size() { return nil, fmt.Errorf("field '%s' (fixed[%d]): provided []byte has length %d, expected %d", fieldName, fixedSchema.Size(), len(byteSlice), fixedSchema.Size()) - } // else it's not for a fixed schema or length mismatch, or it's for 'bytes' type, keep as []byte - + } } } } - // Marshal the potentially coerced arguments return avro.Marshal(schema, coercedArgs) } diff --git a/pkg/schema/avro_core_test.go b/pkg/schema/avro_core_test.go index 37054b3..2043cae 100644 --- a/pkg/schema/avro_core_test.go +++ b/pkg/schema/avro_core_test.go @@ -22,7 +22,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/mark3labs/mcp-go/mcp" ) // AVRO Schema Definitions for Testing diff --git a/pkg/schema/avro_test.go b/pkg/schema/avro_test.go index 58e181c..7308cd9 100644 --- a/pkg/schema/avro_test.go +++ b/pkg/schema/avro_test.go @@ -21,7 +21,6 @@ import ( "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" "github.com/stretchr/testify/assert" - "github.com/mark3labs/mcp-go/mcp" ) // complexRecordSchemaString is used by TestAvroConverter_ValidateArguments diff --git a/pkg/schema/boolean.go b/pkg/schema/boolean.go index b357cab..05f5577 100644 --- a/pkg/schema/boolean.go +++ b/pkg/schema/boolean.go @@ -17,8 +17,8 @@ package schema import ( "fmt" + "github.com/invopop/jsonschema" cliutils "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" "github.com/streamnative/streamnative-mcp-server/pkg/common" ) @@ -36,13 +36,21 @@ func NewBooleanConverter() *BooleanConverter { } } -func (c *BooleanConverter) ToMCPToolInputSchemaProperties(schemaInfo *cliutils.SchemaInfo) ([]mcp.ToolOption, error) { +func (c *BooleanConverter) ToMCPToolInputSchemaProperties(schemaInfo *cliutils.SchemaInfo) (*jsonschema.Schema, error) { if schemaInfo.Type != "BOOLEAN" { return nil, fmt.Errorf("expected BOOLEAN schema, got %s", schemaInfo.Type) } - return []mcp.ToolOption{ - mcp.WithBoolean(c.ParamName, mcp.Description(fmt.Sprintf("The input schema is a %s schema", schemaInfo.Type)), mcp.Required()), + props := jsonschema.NewProperties() + props.Set(c.ParamName, &jsonschema.Schema{ + Type: "boolean", + Description: fmt.Sprintf("The input schema is a %s schema", schemaInfo.Type), + }) + + return &jsonschema.Schema{ + Type: "object", + Properties: props, + Required: []string{c.ParamName}, }, nil } diff --git a/pkg/schema/boolean_test.go b/pkg/schema/boolean_test.go index c01abe1..807b574 100644 --- a/pkg/schema/boolean_test.go +++ b/pkg/schema/boolean_test.go @@ -20,7 +20,6 @@ import ( "testing" "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" "github.com/stretchr/testify/assert" ) diff --git a/pkg/schema/converter.go b/pkg/schema/converter.go index 2c3d6b5..edca1a4 100644 --- a/pkg/schema/converter.go +++ b/pkg/schema/converter.go @@ -17,8 +17,8 @@ package schema import ( "fmt" + "github.com/invopop/jsonschema" cliutils "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" ) const ( @@ -26,7 +26,7 @@ const ( ) type Converter interface { - ToMCPToolInputSchemaProperties(pulsarSchemaInfo *cliutils.SchemaInfo) ([]mcp.ToolOption, error) + ToMCPToolInputSchemaProperties(pulsarSchemaInfo *cliutils.SchemaInfo) (*jsonschema.Schema, error) SerializeMCPRequestToPulsarPayload(arguments map[string]any, targetPulsarSchemaInfo *cliutils.SchemaInfo) ([]byte, error) diff --git a/pkg/schema/json.go b/pkg/schema/json.go index 17bdd8c..f405268 100644 --- a/pkg/schema/json.go +++ b/pkg/schema/json.go @@ -15,11 +15,11 @@ package schema import ( - "encoding/json" // Required for json.Marshal + "encoding/json" "fmt" + "github.com/invopop/jsonschema" cliutils "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" ) // JSONConverter handles the conversion for Pulsar JSON schemas. @@ -35,13 +35,11 @@ func NewJSONConverter() *JSONConverter { } // ToMCPToolInputSchemaProperties converts the Pulsar JSON SchemaInfo (which is AVRO based) -// to MCP tool input schema properties. -func (c *JSONConverter) ToMCPToolInputSchemaProperties(schemaInfo *cliutils.SchemaInfo) ([]mcp.ToolOption, error) { +// to jsonschema.Schema for tool input. +func (c *JSONConverter) ToMCPToolInputSchemaProperties(schemaInfo *cliutils.SchemaInfo) (*jsonschema.Schema, error) { if schemaInfo.Type != "JSON" { - // Assuming GetSchemaType will be available from somewhere in the package (e.g. converter.go) return nil, fmt.Errorf("expected JSON schema, got %s", schemaInfo.Type) } - // The schemaInfo.Schema for JSON type is the AVRO schema string definition. // Delegate to the core AVRO processing function from avro_core.go. return processAvroSchemaStringToMCPToolInput(string(schemaInfo.Schema)) } diff --git a/pkg/schema/json_test.go b/pkg/schema/json_test.go index 5768b78..f8272b7 100644 --- a/pkg/schema/json_test.go +++ b/pkg/schema/json_test.go @@ -21,7 +21,6 @@ import ( "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" "github.com/stretchr/testify/assert" - "github.com/mark3labs/mcp-go/mcp" ) // newJSONSchemaInfo is a helper to create SchemaInfo for JSON type with a given AVRO schema string. diff --git a/pkg/schema/number.go b/pkg/schema/number.go index 622e474..7d60395 100644 --- a/pkg/schema/number.go +++ b/pkg/schema/number.go @@ -18,8 +18,8 @@ import ( "fmt" "math" + "github.com/invopop/jsonschema" cliutils "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" "github.com/streamnative/streamnative-mcp-server/pkg/common" ) @@ -37,13 +37,21 @@ func NewNumberConverter() *NumberConverter { } } -func (c *NumberConverter) ToMCPToolInputSchemaProperties(schemaInfo *cliutils.SchemaInfo) ([]mcp.ToolOption, error) { +func (c *NumberConverter) ToMCPToolInputSchemaProperties(schemaInfo *cliutils.SchemaInfo) (*jsonschema.Schema, error) { if schemaInfo.Type != "INT8" && schemaInfo.Type != "INT16" && schemaInfo.Type != "INT32" && schemaInfo.Type != "INT64" && schemaInfo.Type != "FLOAT" && schemaInfo.Type != "DOUBLE" { return nil, fmt.Errorf("expected INT8, INT16, INT32, INT64, FLOAT, or DOUBLE schema, got %s", schemaInfo.Type) } - return []mcp.ToolOption{ - mcp.WithNumber(c.ParamName, mcp.Description(fmt.Sprintf("The input schema is a %s schema", schemaInfo.Type)), mcp.Required()), + props := jsonschema.NewProperties() + props.Set(c.ParamName, &jsonschema.Schema{ + Type: "number", + Description: fmt.Sprintf("The input schema is a %s schema", schemaInfo.Type), + }) + + return &jsonschema.Schema{ + Type: "object", + Properties: props, + Required: []string{c.ParamName}, }, nil } diff --git a/pkg/schema/number_test.go b/pkg/schema/number_test.go index 0320784..8606f2c 100644 --- a/pkg/schema/number_test.go +++ b/pkg/schema/number_test.go @@ -22,7 +22,6 @@ import ( "github.com/apache/pulsar-client-go/pulsar" cliutils "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" "github.com/stretchr/testify/assert" ) diff --git a/pkg/schema/string.go b/pkg/schema/string.go index 9a4b50a..2383724 100644 --- a/pkg/schema/string.go +++ b/pkg/schema/string.go @@ -17,8 +17,8 @@ package schema import ( "fmt" + "github.com/invopop/jsonschema" cliutils "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" "github.com/streamnative/streamnative-mcp-server/pkg/common" ) @@ -36,13 +36,21 @@ func NewStringConverter() *StringConverter { } } -func (c *StringConverter) ToMCPToolInputSchemaProperties(schemaInfo *cliutils.SchemaInfo) ([]mcp.ToolOption, error) { +func (c *StringConverter) ToMCPToolInputSchemaProperties(schemaInfo *cliutils.SchemaInfo) (*jsonschema.Schema, error) { if schemaInfo.Type != "STRING" && schemaInfo.Type != "BYTES" { return nil, fmt.Errorf("expected STRING or BYTES schema, got %s", schemaInfo.Type) } - return []mcp.ToolOption{ - mcp.WithString(c.ParamName, mcp.Description(fmt.Sprintf("The input schema is a %s schema", schemaInfo.Type)), mcp.Required()), + props := jsonschema.NewProperties() + props.Set(c.ParamName, &jsonschema.Schema{ + Type: "string", + Description: fmt.Sprintf("The input schema is a %s schema", schemaInfo.Type), + }) + + return &jsonschema.Schema{ + Type: "object", + Properties: props, + Required: []string{c.ParamName}, }, nil } diff --git a/pkg/schema/string_test.go b/pkg/schema/string_test.go index c0c9f1f..f6f012b 100644 --- a/pkg/schema/string_test.go +++ b/pkg/schema/string_test.go @@ -20,7 +20,6 @@ import ( "testing" "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" - "github.com/mark3labs/mcp-go/mcp" "github.com/stretchr/testify/assert" ) diff --git a/scripts/pulsar-e2e-entrypoint.sh b/scripts/pulsar-e2e-entrypoint.sh new file mode 100644 index 0000000..cdd4771 --- /dev/null +++ b/scripts/pulsar-e2e-entrypoint.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Copyright 2025 StreamNative +# +# Licensed 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. + +set -e + +echo "Starting Pulsar standalone for E2E testing..." + +# Start Pulsar standalone in foreground +# Using exec to replace the current process ensures proper signal handling +exec bin/pulsar standalone diff --git a/test/docker-compose-pulsar.yml b/test/docker-compose-pulsar.yml new file mode 100644 index 0000000..2ead0bf --- /dev/null +++ b/test/docker-compose-pulsar.yml @@ -0,0 +1,38 @@ +# Copyright 2025 StreamNative +# +# Licensed 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. + +services: + pulsar: + image: apachepulsar/pulsar:3.3.0 + container_name: snmcp-pulsar-standalone + ports: + - "6650:6650" # Pulsar binary protocol + - "8080:8080" # Admin REST API + - "8443:8443" # Pulsar binary protocol TLS + - "8181:8181" # Admin REST API TLS + environment: + - PULSAR_PREFIX=pulsar-standalone + command: > + bin/pulsar standalone + networks: + - pulsar-test + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/admin/v2/clusters"] + interval: 10s + timeout: 5s + retries: 5 + +networks: + pulsar-test: + driver: bridge