#
tokens: 6197/50000 2/2 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── go.mod
├── go.sum
├── main.go
└── README.md
```

# Files

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # MCP Grpcurl
 2 | 
 3 | This project is an Model Context Protocol (MCP) server designed to interact with gRPC services using the `grpcurl` tool. It leverages the `grpcurl` command-line utility to perform various operations on gRPC services, such as invoking methods, listing services, and describing service details.
 4 | 
 5 | ## Features
 6 | 
 7 | - **Invoke gRPC Methods**: Use reflection to invoke gRPC methods with custom headers and JSON payloads.
 8 | - **List gRPC Services**: Retrieve a list of all available gRPC services on the target server.
 9 | - **Describe gRPC Services**: Get detailed descriptions of gRPC services or message types.
10 | 
11 | ## Requirements
12 | 
13 | - Go 1.23.0 or later
14 | - `grpcurl` tool installed on your system
15 | 
16 | ## Setup
17 | 1. install grpcurl: https://github.com/fullstorydev/grpcurl
18 | 
19 | 2. Install the package:
20 |    ```bash
21 |    go install github.com/wricardo/mcp-grpcurl@latest
22 |    ```
23 | 
24 | 3. Configure Cline by adding the following to your MCP settings:
25 |    ```json
26 |    "mcp-grpcurl": {
27 |      "command": "mcp-grpcurl",
28 |      "env": {
29 |        "ADDRESS": "localhost:8005"
30 |      },
31 |      "disabled": false,
32 |      "autoApprove": []
33 |    }
34 |    ```
35 | 
36 | ## Usage
37 | 
38 | Run the MCP server:
39 | ```bash
40 | mcp-grpc-client
41 | ```
42 | 
43 | ### Tools
44 | 
45 | - **invoke**: Invoke a gRPC method using reflection.
46 |   - Parameters:
47 |     - `method`: Fully-qualified method name (e.g., `package.Service/Method`).
48 |     - `request`: JSON payload for the request.
49 |     - `headers`: (Optional) JSON object for custom gRPC headers.
50 | 
51 | - **list**: List all available gRPC services on the target server.
52 | 
53 | - **describe**: Describe a gRPC service or message type.
54 |   - Use dot notation for symbols (e.g., `mypackage.MyService`).
55 | 
```

--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------

```go
  1 | package main
  2 | 
  3 | import (
  4 | 	"bytes"
  5 | 	"context"
  6 | 	"encoding/json"
  7 | 	"fmt"
  8 | 	"io"
  9 | 	"log"
 10 | 	"os"
 11 | 	"strings"
 12 | 	"time"
 13 | 
 14 | 	"github.com/fullstorydev/grpcurl"
 15 | 	"github.com/golang/protobuf/jsonpb"
 16 | 	"github.com/golang/protobuf/proto" //lint:ignore SA1019 same as above
 17 | 	"github.com/jhump/protoreflect/desc"
 18 | 	"github.com/jhump/protoreflect/grpcreflect"
 19 | 	"github.com/mark3labs/mcp-go/mcp"
 20 | 	"github.com/mark3labs/mcp-go/server"
 21 | 	"google.golang.org/grpc"
 22 | 	"google.golang.org/grpc/metadata"
 23 | 	"google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
 24 | 	"google.golang.org/grpc/status"
 25 | 	"google.golang.org/protobuf/types/descriptorpb"
 26 | )
 27 | 
 28 | // customEventHandler extends the DefaultEventHandler to capture headers and trailers
 29 | type customEventHandler struct {
 30 | 	*grpcurl.DefaultEventHandler
 31 | 	headers  metadata.MD
 32 | 	trailers metadata.MD
 33 | }
 34 | 
 35 | // OnReceiveTrailers captures the incoming trailer metadata
 36 | func (h *customEventHandler) OnReceiveTrailers(status *status.Status, md metadata.MD) {
 37 | 	if h.trailers == nil {
 38 | 		h.trailers = md
 39 | 	} else {
 40 | 		for k, v := range md {
 41 | 			h.trailers[k] = append(h.trailers[k], v...)
 42 | 		}
 43 | 	}
 44 | 	h.DefaultEventHandler.OnReceiveTrailers(status, md)
 45 | }
 46 | 
 47 | // NewGrpcReflectionServer creates a new GrpcReflectionServer for the given target address.
 48 | func NewGrpcReflectionServer(host string) *GrpcReflectionServer {
 49 | 	srv := server.NewMCPServer(
 50 | 		"grpcReflectionServer",
 51 | 		"1.0.0",
 52 | 		server.WithLogging(),
 53 | 	)
 54 | 
 55 | 	grs := &GrpcReflectionServer{
 56 | 		srv:     srv,
 57 | 		host:    host,
 58 | 		headers: make(map[string]string),
 59 | 	}
 60 | 
 61 | 	grs.registerTools()
 62 | 	return grs
 63 | }
 64 | 
 65 | // Serve starts the MCP server over standard I/O.
 66 | func (g *GrpcReflectionServer) Serve() error {
 67 | 	return server.ServeStdio(g.srv)
 68 | }
 69 | 
 70 | // registerTools registers the grpcurl-based tools available via the MCP server.
 71 | func (g *GrpcReflectionServer) registerTools() {
 72 | 	// Tool: set-headers
 73 | 	setHeadersTool := mcp.NewTool(
 74 | 		"set-headers",
 75 | 		mcp.WithDescription(`Set global headers to be used with all future gRPC requests.
 76 | Parameters:
 77 |  - "headers": JSON object with header key-value pairs, e.g. {"Authorization": "Bearer <token>"}.
 78 |  - "clear": (Optional) Boolean to clear all existing headers before setting new ones.`),
 79 | 		mcp.WithString("headers", mcp.Description("JSON object with header key-value pairs"), mcp.Required()),
 80 | 		mcp.WithBoolean("clear", mcp.Description("Clear existing headers before setting new ones")),
 81 | 	)
 82 | 	g.srv.AddTool(setHeadersTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 83 | 		args := request.Params.Arguments
 84 | 		headersJSON, _ := args["headers"].(string)
 85 | 		clear, _ := args["clear"].(bool)
 86 | 
 87 | 		// Parse headers
 88 | 		newHeaders := make(map[string]string)
 89 | 		if err := json.Unmarshal([]byte(headersJSON), &newHeaders); err != nil {
 90 | 			return toolError("Failed to parse headers JSON: " + err.Error()), nil
 91 | 		}
 92 | 
 93 | 		// Clear existing headers if requested
 94 | 		if clear {
 95 | 			g.headers = make(map[string]string)
 96 | 		}
 97 | 
 98 | 		// Update headers
 99 | 		for k, v := range newHeaders {
100 | 			g.headers[k] = v
101 | 		}
102 | 
103 | 		// Format headers for display
104 | 		headersMap := make(map[string]interface{})
105 | 		for k, v := range g.headers {
106 | 			headersMap[k] = v
107 | 		}
108 | 
109 | 		jsonResponse, err := json.MarshalIndent(headersMap, "", "  ")
110 | 		if err != nil {
111 | 			return toolError("Failed to marshal headers: " + err.Error()), nil
112 | 		}
113 | 
114 | 		return toolSuccess(fmt.Sprintf("Headers updated successfully:\n%s", string(jsonResponse))), nil
115 | 	})
116 | 
117 | 	// Tool 1: invoke
118 | 	invokeTool := mcp.NewTool(
119 | 		"invoke",
120 | 		mcp.WithDescription(`Invokes a gRPC method using reflection.
121 | Parameters:
122 |  - "method": Fully-qualified method name (e.g., package.Service/Method).
123 |  - "request": JSON payload for the request.
124 |  - "headers": (Optional) JSON object for custom gRPC headers that will be merged with global headers.`),
125 | 		mcp.WithString("method", mcp.Description("Fully-qualified method name (e.g., package.Service/Method)"), mcp.Required()),
126 | 		mcp.WithString("request", mcp.Description("JSON request payload"), mcp.Required()),
127 | 		mcp.WithString("headers", mcp.Description("Optional JSON object for request-specific headers")),
128 | 	)
129 | 	g.srv.AddTool(invokeTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
130 | 		args := request.Params.Arguments
131 | 		method, _ := args["method"].(string)
132 | 		reqPayload, _ := args["request"].(string)
133 | 		requestHeadersJSON, _ := args["headers"].(string)
134 | 
135 | 		// Parse request-specific headers if provided and merge with global headers
136 | 		headers := []string{}
137 | 
138 | 		// Start with global headers
139 | 		for k, v := range g.headers {
140 | 			headers = append(headers, fmt.Sprintf("%s: %s", k, v))
141 | 		}
142 | 
143 | 		// Add request-specific headers if provided
144 | 		if requestHeadersJSON != "" {
145 | 			requestHeaders := make(map[string]string)
146 | 			if err := json.Unmarshal([]byte(requestHeadersJSON), &requestHeaders); err != nil {
147 | 				return toolError("Failed to parse headers JSON: " + err.Error()), nil
148 | 			}
149 | 			for k, v := range requestHeaders {
150 | 				headers = append(headers, fmt.Sprintf("%s: %s", k, v))
151 | 			}
152 | 		}
153 | 
154 | 		// Create a gRPC client connection.
155 | 		network := "tcp"
156 | 		target := g.host
157 | 		dialTime := 10 * time.Second
158 | 
159 | 		dialOptions := []grpc.DialOption{
160 | 			grpc.WithBlock(),
161 | 			grpc.WithTimeout(dialTime),
162 | 			grpc.WithInsecure(), // adjust based on security requirements.
163 | 		}
164 | 
165 | 		cc, err := grpcurl.BlockingDial(ctx, network, target, nil, dialOptions...)
166 | 		if err != nil {
167 | 			return toolError("Failed to create gRPC connection: " + err.Error()), nil
168 | 		}
169 | 		defer cc.Close()
170 | 
171 | 		// Create a reflection client and descriptor source.
172 | 		refClient := grpcreflect.NewClient(ctx, grpc_reflection_v1alpha.NewServerReflectionClient(cc))
173 | 		defer refClient.Reset()
174 | 		descSource := grpcurl.DescriptorSourceFromServer(ctx, refClient)
175 | 
176 | 		// Create an in-memory buffer to capture output.
177 | 		var outputBuffer bytes.Buffer
178 | 
179 | 		// Create a formatter (we don't need the parser in the new API).
180 | 		_, formatter, err := grpcurl.RequestParserAndFormatter(grpcurl.FormatJSON, descSource, &outputBuffer, grpcurl.FormatOptions{})
181 | 		if err != nil {
182 | 			return toolError("Failed to create formatter: " + err.Error()), nil
183 | 		}
184 | 
185 | 		// Create a custom event handler with header capture capability
186 | 		handler := &customEventHandler{
187 | 			DefaultEventHandler: &grpcurl.DefaultEventHandler{
188 | 				Out:            &outputBuffer,
189 | 				Formatter:      formatter,
190 | 				VerbosityLevel: 0,
191 | 				NumResponses:   0,
192 | 				Status:         nil,
193 | 			},
194 | 		}
195 | 
196 | 		// Create a request supplier that supplies a single JSON message.
197 | 		reqSupplier := &singleMessageSupplier{
198 | 			data: []byte(reqPayload),
199 | 		}
200 | 
201 | 		// Invoke the gRPC method using the new API signature.
202 | 		err = grpcurl.InvokeRPC(ctx, descSource, cc, method, headers, handler, reqSupplier.Supply)
203 | 		if err != nil {
204 | 			return toolError("Failed to invoke RPC: " + err.Error()), nil
205 | 		}
206 | 
207 | 		// Check if there was an error status from the RPC
208 | 		if handler.Status != nil && handler.Status.Err() != nil {
209 | 			return toolError(fmt.Sprintf("RPC failed: %v", handler.Status.Err())), nil
210 | 		}
211 | 
212 | 		// Convert metadata.MD to map for JSON marshaling
213 | 		headersMap := metadataToMap(handler.headers)
214 | 		trailersMap := metadataToMap(handler.trailers)
215 | 
216 | 		// Create a structured response with headers and trailers
217 | 		response := map[string]interface{}{
218 | 			"body":     outputBuffer.String(),
219 | 			"headers":  headersMap,
220 | 			"trailers": trailersMap,
221 | 			"metadata": map[string]interface{}{
222 | 				"status_code": handler.Status.Code().String(),
223 | 			},
224 | 		}
225 | 
226 | 		// Convert the response to JSON.
227 | 		jsonResponse, err := json.MarshalIndent(response, "", "  ")
228 | 		if err != nil {
229 | 			return toolError("Failed to marshal response: " + err.Error()), nil
230 | 		}
231 | 
232 | 		// Return the structured response.
233 | 		return toolSuccess(string(jsonResponse)), nil
234 | 	})
235 | 
236 | 	// Tool 2: list
237 | 	listTool := mcp.NewTool(
238 | 		"list",
239 | 		mcp.WithDescription("Lists all available gRPC services on the target server using reflection."),
240 | 	)
241 | 	g.srv.AddTool(listTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
242 | 		// Create a gRPC client connection
243 | 		network := "tcp"
244 | 		target := g.host
245 | 		dialTime := 10 * time.Second
246 | 
247 | 		dialOptions := []grpc.DialOption{
248 | 			grpc.WithBlock(),
249 | 			grpc.WithTimeout(dialTime),
250 | 			grpc.WithInsecure(),
251 | 		}
252 | 
253 | 		cc, err := grpcurl.BlockingDial(ctx, network, target, nil, dialOptions...)
254 | 		if err != nil {
255 | 			return toolError("Failed to create gRPC connection: " + err.Error()), nil
256 | 		}
257 | 		defer cc.Close()
258 | 
259 | 		// Create a reflection client
260 | 		refClient := grpcreflect.NewClient(ctx, grpc_reflection_v1alpha.NewServerReflectionClient(cc))
261 | 		defer refClient.Reset()
262 | 
263 | 		// List all services
264 | 		services, err := refClient.ListServices()
265 | 		if err != nil {
266 | 			return toolError("Failed to list services: " + err.Error()), nil
267 | 		}
268 | 
269 | 		// Format the output similarly to grpcurl
270 | 		var output strings.Builder
271 | 		for _, svc := range services {
272 | 			if svc != "grpc.reflection.v1alpha.ServerReflection" {
273 | 				output.WriteString(svc)
274 | 				output.WriteString("\n")
275 | 			}
276 | 		}
277 | 
278 | 		return toolSuccess(output.String()), nil
279 | 	})
280 | 
281 | 	// Tool 3: describe
282 | 	describeTool := mcp.NewTool(
283 | 		"describe",
284 | 		mcp.WithDescription(`Describes a gRPC service or message type.
285 | Provide the target entity using dot notation.
286 | Examples:
287 |  - "mypackage.MyService" to describe the service.
288 |  - "mypackage.MyMessage.MyRpc" to describe a specific RPC method.
289 |  - "mypackage.MyMessage" to describe a message type.
290 | Note: Slash notation (e.g., "mypackage.MyService/MyMethod") is used for invoking RPCs, not for describing symbols.`),
291 | 		WithStringArray("entities", mcp.Description("The services or messages type to describe (use dot notation)"), mcp.Required()),
292 | 	)
293 | 	g.srv.AddTool(describeTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
294 | 		// Create a gRPC client connection
295 | 		network := "tcp"
296 | 		target := g.host
297 | 		dialTime := 10 * time.Second
298 | 
299 | 		dialOptions := []grpc.DialOption{
300 | 			grpc.WithBlock(),
301 | 			grpc.WithTimeout(dialTime),
302 | 			grpc.WithInsecure(),
303 | 		}
304 | 
305 | 		cc, err := grpcurl.BlockingDial(ctx, network, target, nil, dialOptions...)
306 | 		if err != nil {
307 | 			return toolError("Failed to create gRPC connection: " + err.Error()), nil
308 | 		}
309 | 		defer cc.Close()
310 | 
311 | 		// Create a reflection client and descriptor source
312 | 		refClient := grpcreflect.NewClient(ctx, grpc_reflection_v1alpha.NewServerReflectionClient(cc))
313 | 		defer refClient.Reset()
314 | 		descSource := grpcurl.DescriptorSourceFromServer(ctx, refClient)
315 | 
316 | 		args := request.Params.Arguments
317 | 		entities, ok := args["entities"].(string)
318 | 		var tmp []string
319 | 		if ok {
320 | 			tmp = strings.Split(entities, ",")
321 | 		} else if entities, ok := args["entities"].([]interface{}); ok {
322 | 			for _, entity := range entities {
323 | 				entityStr, ok := entity.(string)
324 | 				if !ok {
325 | 					given, _ := json.Marshal(entity)
326 | 					return toolError(fmt.Sprintf("entities argument should be an array of strings instead of %s", given)), nil
327 | 				}
328 | 				tmp = append(tmp, entityStr)
329 | 			}
330 | 		}
331 | 
332 | 		// Split the entities by comma
333 | 		if len(tmp) == 0 {
334 | 			return toolError("No entities provided"), nil
335 | 		}
336 | 
337 | 		var results []string
338 | 
339 | 		for _, entityStr := range tmp {
340 | 			// Remove leading dot if present
341 | 			if entityStr != "" && entityStr[0] == '.' {
342 | 				entityStr = entityStr[1:]
343 | 			}
344 | 
345 | 			// Find the symbol
346 | 			dsc, err := descSource.FindSymbol(entityStr)
347 | 			if err != nil {
348 | 				return toolError(fmt.Sprintf("Failed to resolve symbol %q: %v", entityStr, err)), nil
349 | 			}
350 | 
351 | 			fqn := dsc.GetFullyQualifiedName()
352 | 			var elementType string
353 | 
354 | 			// Determine the type of the descriptor
355 | 			switch d := dsc.(type) {
356 | 			case *desc.MessageDescriptor:
357 | 				elementType = "a message"
358 | 				if parent, ok := d.GetParent().(*desc.MessageDescriptor); ok {
359 | 					if d.IsMapEntry() {
360 | 						for _, f := range parent.GetFields() {
361 | 							if f.IsMap() && f.GetMessageType() == d {
362 | 								elementType = "the entry type for a map field"
363 | 								dsc = f
364 | 								break
365 | 							}
366 | 						}
367 | 					} else {
368 | 						for _, f := range parent.GetFields() {
369 | 							if f.GetType() == descriptorpb.FieldDescriptorProto_TYPE_GROUP && f.GetMessageType() == d {
370 | 								elementType = "the type of a group field"
371 | 								dsc = f
372 | 								break
373 | 							}
374 | 						}
375 | 					}
376 | 				}
377 | 			case *desc.FieldDescriptor:
378 | 				elementType = "a field"
379 | 				if d.GetType() == descriptorpb.FieldDescriptorProto_TYPE_GROUP {
380 | 					elementType = "a group field"
381 | 				} else if d.IsExtension() {
382 | 					elementType = "an extension"
383 | 				}
384 | 			case *desc.OneOfDescriptor:
385 | 				elementType = "a one-of"
386 | 			case *desc.EnumDescriptor:
387 | 				elementType = "an enum"
388 | 			case *desc.EnumValueDescriptor:
389 | 				elementType = "an enum value"
390 | 			case *desc.ServiceDescriptor:
391 | 				elementType = "a service"
392 | 			case *desc.MethodDescriptor:
393 | 				elementType = "a method"
394 | 			default:
395 | 				return toolError(fmt.Sprintf("descriptor has unrecognized type %T", dsc)), nil
396 | 			}
397 | 
398 | 			// Get the descriptor text
399 | 			txt, err := grpcurl.GetDescriptorText(dsc, descSource)
400 | 			if err != nil {
401 | 				return toolError(fmt.Sprintf("Failed to describe symbol %q: %v", entityStr, err)), nil
402 | 			}
403 | 
404 | 			description := fmt.Sprintf("%s is %s:\n%s", fqn, elementType, txt)
405 | 
406 | 			// // For message types, also show a JSON template
407 | 			// if msgDesc, ok := dsc.(*desc.MessageDescriptor); ok {
408 | 			// 	tmpl := grpcurl.MakeTemplate(msgDesc)
409 | 			// 	options := grpcurl.FormatOptions{EmitJSONDefaultFields: true}
410 | 			// 	_, formatter, err := grpcurl.RequestParserAndFormatter(grpcurl.FormatJSON, descSource, nil, options)
411 | 			// 	if err != nil {
412 | 			// 		return toolError(fmt.Sprintf("Failed to create formatter: %v", err)), nil
413 | 			// 	}
414 | 			// 	str, err := formatter(tmpl)
415 | 			// 	if err != nil {
416 | 			// 		return toolError(fmt.Sprintf("Failed to print template for message %s: %v", entityStr, err)), nil
417 | 			// 	}
418 | 			// 	description += "\nMessage template:\n" + str
419 | 			// }
420 | 
421 | 			results = append(results, description)
422 | 		}
423 | 
424 | 		return toolSuccess(strings.Join(results, "\n\n")), nil
425 | 	})
426 | 
427 | 	return
428 | }
429 | 
430 | // metadataToMap converts gRPC metadata to a map suitable for JSON marshaling
431 | func metadataToMap(md metadata.MD) map[string]interface{} {
432 | 	result := make(map[string]interface{})
433 | 	for key, values := range md {
434 | 		// If there's only one value, store it directly rather than as an array
435 | 		if len(values) == 1 {
436 | 			result[key] = values[0]
437 | 		} else if len(values) > 0 {
438 | 			result[key] = values
439 | 		}
440 | 	}
441 | 	return result
442 | }
443 | 
444 | // singleMessageSupplier implements grpcurl.RequestSupplier interface for a single message.
445 | type singleMessageSupplier struct {
446 | 	data []byte
447 | 	used bool
448 | }
449 | 
450 | // Supply implements the grpcurl.RequestSupplier interface.
451 | func (s *singleMessageSupplier) Supply(msg proto.Message) error {
452 | 	if s.used {
453 | 		return io.EOF
454 | 	}
455 | 	s.used = true
456 | 	return jsonpb.Unmarshal(bytes.NewReader(s.data), msg)
457 | }
458 | 
459 | func main() {
460 | 	address := os.Getenv("ADDRESS")
461 | 	if address == "" {
462 | 		log.Fatal("ADDRESS environment variable is required")
463 | 		os.Exit(1)
464 | 	}
465 | 	grpcServer := NewGrpcReflectionServer(address)
466 | 	if err := grpcServer.Serve(); err != nil && err != io.EOF {
467 | 		log.Fatal("Error serving MCP server:", err)
468 | 		os.Exit(1)
469 | 	}
470 | }
471 | 
472 | // WithStringArray adds a string array property to the tool schema.
473 | func WithStringArray(name string, opts ...mcp.PropertyOption) mcp.ToolOption {
474 | 	return func(t *mcp.Tool) {
475 | 		schema := map[string]interface{}{
476 | 			"type": "array",
477 | 			"items": map[string]interface{}{
478 | 				"type": "string",
479 | 			},
480 | 		}
481 | 
482 | 		for _, opt := range opts {
483 | 			opt(schema)
484 | 		}
485 | 
486 | 		if required, ok := schema["required"].(bool); ok && required {
487 | 			delete(schema, "required")
488 | 			if t.InputSchema.Required == nil {
489 | 				t.InputSchema.Required = []string{name}
490 | 			} else {
491 | 				t.InputSchema.Required = append(t.InputSchema.Required, name)
492 | 			}
493 | 		}
494 | 
495 | 		t.InputSchema.Properties[name] = schema
496 | 	}
497 | }
498 | 
499 | // toolSuccess creates a successful MCP response with the provided text contents.
500 | func toolSuccess(contents ...string) *mcp.CallToolResult {
501 | 	var iface []interface{}
502 | 	for _, c := range contents {
503 | 		iface = append(iface, mcp.NewTextContent(c))
504 | 	}
505 | 	return &mcp.CallToolResult{
506 | 		Content: iface,
507 | 		IsError: false,
508 | 	}
509 | }
510 | 
511 | // toolError creates an MCP error response with the given error message.
512 | func toolError(message string) *mcp.CallToolResult {
513 | 	return &mcp.CallToolResult{
514 | 		Content: []interface{}{mcp.NewTextContent(message)},
515 | 		IsError: true,
516 | 	}
517 | }
518 | 
519 | // GrpcReflectionServer wraps grpcurl functionalities into an MCP server.
520 | type GrpcReflectionServer struct {
521 | 	srv     *server.MCPServer
522 | 	host    string
523 | 	headers map[string]string // Global headers to be used with all requests
524 | }
525 | 
```