Skip to content

Commit 70f9978

Browse files
authored
Support mcp server (#303)
* support mcp server * add type assertion
1 parent fa8fc72 commit 70f9978

File tree

4 files changed

+238
-1
lines changed

4 files changed

+238
-1
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# How to use gRPC Federation
2+
3+
## Understanding gRPC Federation
4+
5+
First, read https://deepwiki.com/mercari/grpc-federation to understand gRPC Federation.
6+
7+
The `grpc/federation/federation.proto` is defined at https://github.com/mercari/grpc-federation/blob/main/proto/grpc/federation/federation.proto.
8+
9+
The schema defined in this proto represents the options configurable in gRPC Federation.
10+
11+
Detailed explanations of these options can be found at https://github.com/mercari/grpc-federation/blob/main/docs/references.md.
12+
13+
When describing gRPC Federation options, implement them based on this information.
14+
15+
## Practices for Describing gRPC Federation Options
16+
17+
- If you want to validate or process errors returned from dependent microservices, use the `(grpc.federation.message).def.call.error` option.
18+
- Avoid creating messages unrelated to the response structure of the microservice you are building.
19+
- If the same value is always returned, use Service Variable. Otherwise, create a message only when the same process is used in multiple places and the process itself is complex.
20+
If the process is not complex, even if it is the same process, it is preferable to write the process directly in the existing message. Actively use the alias functionality of messages or enums when simple mapping suffices.
21+
- To avoid redundant `grpc.federation.field` option descriptions, actively use the `autobind` feature.
22+
- You can associate enum values with strings using `(grpc.federation.enum_value).attr`. Use this feature actively if necessary.
23+
- gRPC Federation provides a wide range of standard libraries starting with `grpc.federation.*`. Consider whether these features can be utilized as needed.
24+
- When the package is the same, you can omit the package prefix when referencing other messages or enums using `(grpc.federation.message).def.message` or `(grpc.federation.message).def.enum`. Otherwise, always include the package prefix.
25+
- The CEL specification is available at https://github.com/google/cel-spec/blob/master/doc/langdef.md.
26+
- Additionally, in gRPC Federation, you can use the optional keyword (`?`). Always consider using optional when the existence of the target value cannot be guaranteed during field access or index access (the optional feature is also explained at https://pkg.go.dev/github.com/google/cel-go/cel#hdr-Syntax_Changes-OptionalTypes).
27+
- Use `custom_resolver` only as a last resort. Use it only when it is impossible to express something in proto.
28+
29+
## Examples of gRPC Federation
30+
31+
To learn more about how to describe gRPC Federation options, refer to the examples at https://github.com/mercari/grpc-federation/tree/main/_examples.
32+
Each example includes Go language files (`*_grpc_federation.pb.go`) and test codes generated automatically from the proto descriptions. Read these codes as needed to understand the meaning of the options.
33+
34+
## When Changing gRPC Federation Options
35+
36+
When changing gRPC Federation options, always compile the target proto file and repeat the process until it succeeds to verify the correctness of the description.
37+
38+
Follow the steps below to compile the proto file:
39+
40+
1. Extract the list of import files from the proto file to be compiled. Use the `get_import_proto_list` tool for extraction.
41+
2. Create the absolute path of the import path required to compile the obtained import file list. Analyze the current repository structure to use the correct information.
42+
3. Repeat steps 1 to 2 for the obtained import paths to create a unique and complete list of import paths required for compilation.
43+
4. Use the `compile_proto` tool with the extracted import path list to compile the target proto file.
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"embed"
6+
"encoding/json"
7+
"fmt"
8+
"log"
9+
"os"
10+
11+
"github.com/mark3labs/mcp-go/mcp"
12+
"github.com/mark3labs/mcp-go/server"
13+
14+
"github.com/mercari/grpc-federation/source"
15+
"github.com/mercari/grpc-federation/validator"
16+
)
17+
18+
//go:embed assets/*
19+
var assets embed.FS
20+
21+
func main() {
22+
docs, err := assets.ReadFile("assets/docs.md")
23+
if err != nil {
24+
log.Fatalf("failed to read document: %v", err)
25+
}
26+
27+
// Create a new MCP server
28+
s := server.NewMCPServer(
29+
"gRPC Federation MCP Server",
30+
"1.0.0",
31+
server.WithToolCapabilities(false),
32+
server.WithResourceCapabilities(false, false),
33+
server.WithRecovery(),
34+
server.WithInstructions(string(docs)),
35+
)
36+
37+
s.AddTool(
38+
mcp.NewTool(
39+
"get_import_proto_list",
40+
mcp.WithDescription("Returns a list of import proto files used in the specified proto file"),
41+
mcp.WithString(
42+
"path",
43+
mcp.Description("The absolute path to the proto file to be analyzed"),
44+
mcp.Required(),
45+
),
46+
),
47+
func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
48+
args, ok := r.Params.Arguments.(map[string]any)
49+
if !ok {
50+
return nil, fmt.Errorf("unexpected argument format: %T", r.Params.Arguments)
51+
}
52+
path, ok := args["path"].(string)
53+
if !ok {
54+
return nil, fmt.Errorf("failed to find path parameter from arguments: %v", args)
55+
}
56+
content, err := os.ReadFile(path)
57+
if err != nil {
58+
return nil, fmt.Errorf("failed to read file: %w", err)
59+
}
60+
f, err := source.NewFile(path, content)
61+
if err != nil {
62+
return nil, err
63+
}
64+
list, err := json.Marshal(append(f.Imports(), f.ImportsByImportRule()...))
65+
if err != nil {
66+
return nil, err
67+
}
68+
return &mcp.CallToolResult{
69+
Content: []mcp.Content{
70+
mcp.TextContent{
71+
Type: "text",
72+
Text: string(list),
73+
},
74+
},
75+
}, nil
76+
},
77+
)
78+
79+
s.AddTool(
80+
mcp.NewTool(
81+
"compile_proto",
82+
mcp.WithDescription("Compile the proto file using the gRPC Federation option"),
83+
mcp.WithString(
84+
"path",
85+
mcp.Description("The absolute path to the proto file to be analyzed"),
86+
mcp.Required(),
87+
),
88+
mcp.WithArray(
89+
"import_paths",
90+
mcp.Description("Specify the list of import paths required to locate dependency files during compilation. It is recommended to obtain this list using the get_import_proto_list tool"),
91+
mcp.Required(),
92+
),
93+
),
94+
func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
95+
args, ok := r.Params.Arguments.(map[string]any)
96+
if !ok {
97+
return nil, fmt.Errorf("unexpected argument format: %T", r.Params.Arguments)
98+
}
99+
path, ok := args["path"].(string)
100+
if !ok {
101+
return nil, fmt.Errorf("failed to find path parameter from arguments: %v", args)
102+
}
103+
importPathsArg, ok := args["import_paths"].([]any)
104+
if !ok {
105+
return nil, fmt.Errorf("failed to find import_paths parameter from arguments: %v", args)
106+
}
107+
content, err := os.ReadFile(path)
108+
if err != nil {
109+
return nil, fmt.Errorf("failed to read file: %w", err)
110+
}
111+
file, err := source.NewFile(path, content)
112+
if err != nil {
113+
return nil, err
114+
}
115+
importPaths := make([]string, 0, len(importPathsArg))
116+
for _, p := range importPathsArg {
117+
importPath, ok := p.(string)
118+
if !ok {
119+
return nil, fmt.Errorf("failed to get import_paths element. required type is string but got %T", p)
120+
}
121+
importPaths = append(importPaths, importPath)
122+
}
123+
v := validator.New()
124+
outs := v.Validate(context.Background(), file, validator.ImportPathOption(importPaths...))
125+
if validator.ExistsError(outs) {
126+
return nil, fmt.Errorf("failed to compile:\n%s", validator.Format(outs))
127+
}
128+
return &mcp.CallToolResult{
129+
Content: []mcp.Content{
130+
mcp.TextContent{
131+
Type: "text",
132+
Text: "build successful",
133+
},
134+
},
135+
}, nil
136+
},
137+
)
138+
139+
s.AddResource(
140+
mcp.NewResource(
141+
"grpc-federation",
142+
"grpc-federation",
143+
mcp.WithResourceDescription("gRPC Federation Document"),
144+
mcp.WithMIMEType("text/markdown"),
145+
),
146+
func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
147+
return []mcp.ResourceContents{
148+
&mcp.TextResourceContents{
149+
URI: "https://github.com/mercari/grpc-federation",
150+
MIMEType: "text/markdown",
151+
Text: string(docs),
152+
},
153+
}, nil
154+
},
155+
)
156+
157+
s.AddPrompt(
158+
mcp.NewPrompt(
159+
"grpc_federation",
160+
mcp.WithPromptDescription("How to use gRPC Federation"),
161+
),
162+
func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
163+
return mcp.NewGetPromptResult("How to use gRPC Federation",
164+
[]mcp.PromptMessage{
165+
mcp.NewPromptMessage(
166+
mcp.RoleAssistant,
167+
mcp.NewTextContent(string(docs)),
168+
),
169+
},
170+
), nil
171+
},
172+
)
173+
174+
// Start the server
175+
if err := server.ServeStdio(s); err != nil {
176+
log.Fatalf("failed to start server: %v", err)
177+
}
178+
}

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ require (
1414
github.com/google/uuid v1.6.0
1515
github.com/jessevdk/go-flags v1.5.0
1616
github.com/kelseyhightower/envconfig v1.4.0
17+
github.com/mark3labs/mcp-go v0.32.0
1718
github.com/tetratelabs/wazero v1.9.0
1819
go.lsp.dev/jsonrpc2 v0.10.0
1920
go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2
@@ -40,9 +41,10 @@ require (
4041
github.com/mattn/go-isatty v0.0.17 // indirect
4142
github.com/segmentio/asm v1.1.3 // indirect
4243
github.com/segmentio/encoding v0.3.4 // indirect
44+
github.com/spf13/cast v1.7.1 // indirect
4345
github.com/stealthrocket/wazergo v0.19.1 // indirect
4446
github.com/stoewer/go-strcase v1.3.0 // indirect
45-
github.com/stretchr/testify v1.9.0 // indirect
47+
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
4648
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
4749
golang.org/x/net v0.38.0 // indirect
4850
golang.org/x/sys v0.31.0 // indirect

go.sum

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
1111
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1212
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
1313
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
14+
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
15+
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
1416
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
1517
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
1618
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
@@ -36,8 +38,14 @@ github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LF
3638
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
3739
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
3840
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
41+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
42+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
43+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
44+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
3945
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
4046
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
47+
github.com/mark3labs/mcp-go v0.32.0 h1:fgwmbfL2gbd67obg57OfV2Dnrhs1HtSdlY/i5fn7MU8=
48+
github.com/mark3labs/mcp-go v0.32.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
4149
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
4250
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
4351
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
@@ -47,10 +55,14 @@ github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPn
4755
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
4856
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4957
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
58+
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
59+
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
5060
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
5161
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
5262
github.com/segmentio/encoding v0.3.4 h1:WM4IBnxH8B9TakiM2QD5LyNl9JSndh88QbHqVC+Pauc=
5363
github.com/segmentio/encoding v0.3.4/go.mod h1:n0JeuIqEQrQoPDGsjo8UNd1iA0U8d8+oHAA4E3G3OxM=
64+
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
65+
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
5466
github.com/stealthrocket/wazergo v0.19.1 h1:BPrITETPgSFwiytwmToO0MbUC/+RGC39JScz1JmmG6c=
5567
github.com/stealthrocket/wazergo v0.19.1/go.mod h1:riI0hxw4ndZA5e6z7PesHg2BtTftcZaMxRcoiGGipTs=
5668
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
@@ -65,6 +77,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
6577
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
6678
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
6779
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
80+
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
81+
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
6882
go.lsp.dev/jsonrpc2 v0.10.0 h1:Pr/YcXJoEOTMc/b6OTmcR1DPJ3mSWl/SWiU1Cct6VmI=
6983
go.lsp.dev/jsonrpc2 v0.10.0/go.mod h1:fmEzIdXPi/rf6d4uFcayi8HpFP1nBF99ERP1htC72Ac=
7084
go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 h1:hCzQgh6UcwbKgNSRurYWSqh8MufqRRPODRBblutn4TE=

0 commit comments

Comments
 (0)