Skip to content

Commit 303be4e

Browse files
committed
feat: add MCP server for AI assistant integration
Add Model Context Protocol (MCP) server to enable AI assistants like Claude to search and query FQDNs without using the web UI. New MCP tools: - search_fqdns: Search FQDNs with filters (query, source, group, portal, namespace) - list_portals: List all available portals - get_fqdn_details: Get detailed information about a specific FQDN New CLI flags: - --enable-mcp: Enable the MCP server (default: false) - --mcp-transport: Transport type 'stdio' or 'sse' (default: stdio) - --mcp-bind-address: SSE server address (default: :8081) Test coverage: 89.4% for the MCP package (32 tests)
1 parent 1957def commit 303be4e

8 files changed

Lines changed: 1701 additions & 1 deletion

File tree

cmd/main.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import (
4343
"github.com/golgoth31/sreportal/internal/config"
4444
"github.com/golgoth31/sreportal/internal/controller"
4545
portalctrl "github.com/golgoth31/sreportal/internal/controller/portal"
46+
"github.com/golgoth31/sreportal/internal/mcp"
4647
webhookv1alpha1 "github.com/golgoth31/sreportal/internal/webhook/v1alpha1"
4748
"github.com/golgoth31/sreportal/internal/webserver"
4849
// +kubebuilder:scaffold:imports
@@ -76,6 +77,9 @@ func main() {
7677
var enableHTTP2 bool
7778
var configPath string
7879
var portalNamespace string
80+
var enableMCP bool
81+
var mcpTransport string
82+
var mcpAddr string
7983
var tlsOpts []func(*tls.Config)
8084
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
8185
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
@@ -102,6 +106,12 @@ func main() {
102106
flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.")
103107
flag.BoolVar(&enableHTTP2, "enable-http2", false,
104108
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
109+
flag.BoolVar(&enableMCP, "enable-mcp", false,
110+
"If set, the MCP (Model Context Protocol) server will be enabled for AI assistant integration.")
111+
flag.StringVar(&mcpTransport, "mcp-transport", "stdio",
112+
"The transport to use for the MCP server: 'stdio' or 'sse'.")
113+
flag.StringVar(&mcpAddr, "mcp-bind-address", ":8081",
114+
"The address the MCP SSE server binds to (only used when mcp-transport is 'sse').")
105115
opts := zap.Options{
106116
Development: true,
107117
}
@@ -326,6 +336,30 @@ func main() {
326336
}
327337
}()
328338

339+
// Start MCP server if enabled
340+
if enableMCP {
341+
mcpServer := mcp.New(mgr.GetClient(), &operatorConfig.GroupMapping)
342+
switch mcpTransport {
343+
case "stdio":
344+
go func() {
345+
setupLog.Info("starting MCP server", "transport", "stdio")
346+
if err := mcpServer.ServeStdio(); err != nil {
347+
setupLog.Error(err, "MCP server error")
348+
}
349+
}()
350+
case "sse":
351+
go func() {
352+
setupLog.Info("starting MCP server", "transport", "sse", "address", mcpAddr)
353+
if err := mcpServer.ServeSSE(mcpAddr); err != nil {
354+
setupLog.Error(err, "MCP server error")
355+
}
356+
}()
357+
default:
358+
setupLog.Error(nil, "unknown MCP transport", "transport", mcpTransport)
359+
os.Exit(1)
360+
}
361+
}
362+
329363
ctx := ctrl.SetupSignalHandler()
330364

331365
setupLog.Info("starting manager")

go.mod

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ go 1.25.3
55
require (
66
connectrpc.com/connect v1.19.1
77
github.com/labstack/echo/v5 v5.0.3
8+
github.com/mark3labs/mcp-go v0.44.0
89
github.com/onsi/ginkgo/v2 v2.27.2
910
github.com/onsi/gomega v1.38.2
11+
github.com/stretchr/testify v1.11.1
1012
golang.org/x/net v0.49.0
1113
google.golang.org/protobuf v1.36.10
1214
istio.io/client-go v1.28.0
@@ -26,8 +28,10 @@ require (
2628
github.com/alecthomas/kingpin/v2 v2.4.0 // indirect
2729
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
2830
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
31+
github.com/bahlo/generic-list-go v0.2.0 // indirect
2932
github.com/beorn7/perks v1.0.1 // indirect
3033
github.com/blang/semver/v4 v4.0.0 // indirect
34+
github.com/buger/jsonparser v1.1.1 // indirect
3135
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
3236
github.com/cespare/xxhash/v2 v2.3.0 // indirect
3337
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 // indirect
@@ -55,6 +59,7 @@ require (
5559
github.com/google/uuid v1.6.0 // indirect
5660
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
5761
github.com/inconshreveable/mousetrap v1.1.0 // indirect
62+
github.com/invopop/jsonschema v0.13.0 // indirect
5863
github.com/josharian/intern v1.0.0 // indirect
5964
github.com/json-iterator/go v1.1.12 // indirect
6065
github.com/mailru/easyjson v0.9.0 // indirect
@@ -72,13 +77,15 @@ require (
7277
github.com/prometheus/procfs v0.17.0 // indirect
7378
github.com/sirupsen/logrus v1.9.3 // indirect
7479
github.com/smartystreets/goconvey v1.7.2 // indirect
80+
github.com/spf13/cast v1.7.1 // indirect
7581
github.com/spf13/cobra v1.10.1 // indirect
7682
github.com/spf13/pflag v1.0.9 // indirect
7783
github.com/stoewer/go-strcase v1.3.0 // indirect
7884
github.com/stretchr/objx v0.5.2 // indirect
79-
github.com/stretchr/testify v1.11.1 // indirect
85+
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
8086
github.com/x448/float16 v0.8.4 // indirect
8187
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
88+
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
8289
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
8390
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
8491
go.opentelemetry.io/otel v1.37.0 // indirect

go.sum

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQ
8080
github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
8181
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
8282
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
83+
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
84+
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
8385
github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
8486
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
8587
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@@ -93,6 +95,8 @@ github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM
9395
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
9496
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
9597
github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
98+
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
99+
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
96100
github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
97101
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
98102
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
@@ -206,6 +210,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
206210
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
207211
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
208212
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
213+
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
214+
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
209215
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
210216
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
211217
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
@@ -430,6 +436,8 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
430436
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
431437
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
432438
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
439+
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
440+
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
433441
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
434442
github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
435443
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
@@ -500,6 +508,8 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN
500508
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
501509
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
502510
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
511+
github.com/mark3labs/mcp-go v0.44.0 h1:OlYfcVviAnwNN40QZUrrzU0QZjq3En7rCU5X09a/B7I=
512+
github.com/mark3labs/mcp-go v0.44.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
503513
github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=
504514
github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11 h1:YFh+sjyJTMQSYjKwM4dFKhJPJC/wfo98tPUc17HdoYw=
505515
github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11/go.mod h1:Ah2dBMoxZEqk118as2T4u4fjfXarE0pPnMJaArZQZsI=
@@ -711,6 +721,8 @@ github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B
711721
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
712722
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
713723
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
724+
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
725+
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
714726
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
715727
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
716728
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
@@ -771,6 +783,8 @@ github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/
771783
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
772784
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
773785
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
786+
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
787+
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
774788
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
775789
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
776790
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@@ -784,6 +798,8 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMx
784798
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
785799
github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8=
786800
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
801+
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
802+
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
787803
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
788804
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
789805
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=

internal/mcp/get_fqdn_details.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
Copyright 2026.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package mcp
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
"fmt"
23+
"strings"
24+
25+
"github.com/mark3labs/mcp-go/mcp"
26+
27+
sreportalv1alpha1 "github.com/golgoth31/sreportal/api/v1alpha1"
28+
"github.com/golgoth31/sreportal/internal/adapter"
29+
)
30+
31+
// FQDNDetails represents detailed information about a specific FQDN
32+
type FQDNDetails struct {
33+
Name string `json:"name"`
34+
Source string `json:"source"`
35+
Group string `json:"group"`
36+
Description string `json:"description,omitempty"`
37+
RecordType string `json:"record_type"`
38+
Targets []string `json:"targets"`
39+
Portal string `json:"portal,omitempty"`
40+
Namespace string `json:"namespace,omitempty"`
41+
LastSeen string `json:"last_seen,omitempty"`
42+
DNSResource string `json:"dns_resource,omitempty"`
43+
}
44+
45+
// handleGetFQDNDetails handles the get_fqdn_details tool call
46+
func (s *Server) handleGetFQDNDetails(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
47+
// Extract required parameter
48+
fqdn, err := request.RequireString("fqdn")
49+
if err != nil {
50+
return mcp.NewToolResultError("fqdn parameter is required"), nil
51+
}
52+
53+
// Normalize the FQDN for comparison
54+
fqdnLower := strings.ToLower(strings.TrimSuffix(fqdn, "."))
55+
56+
// Search in all DNS resources
57+
var dnsList sreportalv1alpha1.DNSList
58+
if err := s.client.List(ctx, &dnsList); err != nil {
59+
return mcp.NewToolResultError(fmt.Sprintf("failed to list DNS resources: %v", err)), nil
60+
}
61+
62+
// Look for the FQDN in DNS status
63+
for _, dns := range dnsList.Items {
64+
for _, grp := range dns.Status.Groups {
65+
for _, fqdnStatus := range grp.FQDNs {
66+
fqdnStatusLower := strings.ToLower(strings.TrimSuffix(fqdnStatus.FQDN, "."))
67+
if fqdnStatusLower == fqdnLower {
68+
details := FQDNDetails{
69+
Name: fqdnStatus.FQDN,
70+
Source: grp.Source,
71+
Group: grp.Name,
72+
Description: fqdnStatus.Description,
73+
RecordType: fqdnStatus.RecordType,
74+
Targets: fqdnStatus.Targets,
75+
Portal: dns.Spec.PortalRef,
76+
Namespace: dns.Namespace,
77+
DNSResource: fmt.Sprintf("%s/%s", dns.Namespace, dns.Name),
78+
}
79+
if !fqdnStatus.LastSeen.IsZero() {
80+
details.LastSeen = fqdnStatus.LastSeen.Format("2006-01-02T15:04:05Z07:00")
81+
}
82+
83+
jsonBytes, err := json.MarshalIndent(details, "", " ")
84+
if err != nil {
85+
return mcp.NewToolResultError(fmt.Sprintf("failed to marshal details: %v", err)), nil
86+
}
87+
88+
return mcp.NewToolResultText(fmt.Sprintf("FQDN details for '%s':\n\n%s", fqdn, string(jsonBytes))), nil
89+
}
90+
}
91+
}
92+
}
93+
94+
// Also check DNSRecords directly
95+
var dnsRecordList sreportalv1alpha1.DNSRecordList
96+
if err := s.client.List(ctx, &dnsRecordList); err != nil {
97+
return mcp.NewToolResultError(fmt.Sprintf("failed to list DNSRecord resources: %v", err)), nil
98+
}
99+
100+
var allEndpoints []sreportalv1alpha1.EndpointStatus
101+
for _, rec := range dnsRecordList.Items {
102+
allEndpoints = append(allEndpoints, rec.Status.Endpoints...)
103+
}
104+
105+
if len(allEndpoints) > 0 {
106+
groups := adapter.EndpointStatusToGroups(allEndpoints, s.groupMapping)
107+
for _, grp := range groups {
108+
for _, fqdnStatus := range grp.FQDNs {
109+
fqdnStatusLower := strings.ToLower(strings.TrimSuffix(fqdnStatus.FQDN, "."))
110+
if fqdnStatusLower == fqdnLower {
111+
details := FQDNDetails{
112+
Name: fqdnStatus.FQDN,
113+
Source: grp.Source,
114+
Group: grp.Name,
115+
RecordType: fqdnStatus.RecordType,
116+
Targets: fqdnStatus.Targets,
117+
}
118+
if !fqdnStatus.LastSeen.IsZero() {
119+
details.LastSeen = fqdnStatus.LastSeen.Format("2006-01-02T15:04:05Z07:00")
120+
}
121+
122+
jsonBytes, err := json.MarshalIndent(details, "", " ")
123+
if err != nil {
124+
return mcp.NewToolResultError(fmt.Sprintf("failed to marshal details: %v", err)), nil
125+
}
126+
127+
return mcp.NewToolResultText(fmt.Sprintf("FQDN details for '%s':\n\n%s", fqdn, string(jsonBytes))), nil
128+
}
129+
}
130+
}
131+
}
132+
133+
return mcp.NewToolResultText(fmt.Sprintf("FQDN '%s' not found.", fqdn)), nil
134+
}

internal/mcp/list_portals.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
Copyright 2026.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package mcp
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
"fmt"
23+
24+
"github.com/mark3labs/mcp-go/mcp"
25+
26+
sreportalv1alpha1 "github.com/golgoth31/sreportal/api/v1alpha1"
27+
)
28+
29+
// PortalResult represents a portal in the list results
30+
type PortalResult struct {
31+
Name string `json:"name"`
32+
Namespace string `json:"namespace"`
33+
Title string `json:"title"`
34+
Main bool `json:"main"`
35+
SubPath string `json:"subPath,omitempty"`
36+
RemoteURL string `json:"remoteUrl,omitempty"`
37+
Ready bool `json:"ready"`
38+
}
39+
40+
// handleListPortals handles the list_portals tool call
41+
func (s *Server) handleListPortals(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
42+
// List all Portal resources
43+
var portalList sreportalv1alpha1.PortalList
44+
if err := s.client.List(ctx, &portalList); err != nil {
45+
return mcp.NewToolResultError(fmt.Sprintf("failed to list Portal resources: %v", err)), nil
46+
}
47+
48+
if len(portalList.Items) == 0 {
49+
return mcp.NewToolResultText("No portals found."), nil
50+
}
51+
52+
// Convert to results
53+
results := make([]PortalResult, 0, len(portalList.Items))
54+
for _, portal := range portalList.Items {
55+
result := PortalResult{
56+
Name: portal.Name,
57+
Namespace: portal.Namespace,
58+
Title: portal.Spec.Title,
59+
Main: portal.Spec.Main,
60+
SubPath: portal.Spec.SubPath,
61+
Ready: portal.Status.Ready,
62+
}
63+
if portal.Spec.Remote != nil {
64+
result.RemoteURL = portal.Spec.Remote.URL
65+
}
66+
results = append(results, result)
67+
}
68+
69+
jsonBytes, err := json.MarshalIndent(results, "", " ")
70+
if err != nil {
71+
return mcp.NewToolResultError(fmt.Sprintf("failed to marshal results: %v", err)), nil
72+
}
73+
74+
return mcp.NewToolResultText(fmt.Sprintf("Found %d portal(s):\n\n%s", len(results), string(jsonBytes))), nil
75+
}

0 commit comments

Comments
 (0)