Skip to content

Commit 569e98d

Browse files
committed
feat(mcp): add MCP server
1 parent ee14479 commit 569e98d

24 files changed

Lines changed: 1333 additions & 48 deletions

api/handler.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
type Handler interface {
2323
http.Handler
2424
RegisterHealthHandler(path string, h http.Handler)
25+
RegisterMcpHandler(path string, h http.Handler)
2526
}
2627

2728
type handler struct {
@@ -33,6 +34,8 @@ type handler struct {
3334
index string
3435
healthPath string
3536
healthHandler http.Handler
37+
mcpPath string
38+
mcpHandler http.Handler
3639
}
3740

3841
type info struct {
@@ -111,6 +114,11 @@ func (h *handler) RegisterHealthHandler(path string, handler http.Handler) {
111114
h.healthHandler = handler
112115
}
113116

117+
func (h *handler) RegisterMcpHandler(path string, handler http.Handler) {
118+
h.mcpPath = path
119+
h.mcpHandler = handler
120+
}
121+
114122
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
115123
if r.Method != "GET" && r.Method != "POST" {
116124
http.Error(w, fmt.Sprintf("method %v is not allowed", r.Method), http.StatusMethodNotAllowed)
@@ -154,6 +162,8 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
154162
h.getSearchResults(w, r)
155163
case strings.HasPrefix(p, h.healthPath) && h.healthHandler != nil:
156164
h.healthHandler.ServeHTTP(w, r)
165+
case strings.HasPrefix(p, h.mcpPath) && h.mcpHandler != nil:
166+
h.mcpHandler.ServeHTTP(w, r)
157167
case h.fileServer != nil:
158168
if r.Method != "GET" {
159169
http.Error(w, fmt.Sprintf("method %v is not allowed", r.Method), http.StatusMethodNotAllowed)

cmd/mokapi/main_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ health:
8989
path: /health
9090
port: 8080
9191
log: false
92+
mcp:
93+
server:
94+
enabled: false
95+
path: /mcp
96+
port: 8080
9297
rootCaCert: ""
9398
rootCaKey: ""
9499
configs: []

config/static/static_config.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type Config struct {
1919
Providers Providers `json:"providers" yaml:"providers"`
2020
Api Api `json:"api" yaml:"api"`
2121
Health Health `json:"health" yaml:"health"`
22+
Mcp Mcp `json:"mcp" yaml:"mcp"`
2223
RootCaCert tls.FileOrContent `json:"rootCaCert" yaml:"rootCaCert" name:"root-ca-cert"`
2324
RootCaKey tls.FileOrContent `json:"rootCaKey" yaml:"rootCaKey" name:"root-ca-cert"`
2425
Configs Configs `json:"configs" yaml:"configs" explode:"config"`
@@ -44,6 +45,14 @@ func NewConfig() *Config {
4445
cfg.Health.Port = 8080
4546
cfg.Health.Path = "/health"
4647

48+
cfg.Mcp = Mcp{
49+
Server: McpServer{
50+
Enabled: false,
51+
Port: 8080,
52+
Path: "/mcp",
53+
},
54+
}
55+
4756
cfg.Providers.File.SkipPrefix = []string{"_"}
4857
cfg.Event.Store = map[string]Store{"default": {Size: 100}}
4958
cfg.DataGen.OptionalProperties = "0.85"
@@ -298,3 +307,13 @@ type Health struct {
298307
Port int `yaml:"port" json:"port"`
299308
Log bool `yaml:"log" json:"log"`
300309
}
310+
311+
type Mcp struct {
312+
Server McpServer `yaml:"server" json:"server"`
313+
}
314+
315+
type McpServer struct {
316+
Enabled bool `yaml:"enabled" json:"enabled"`
317+
Path string `yaml:"path" json:"path"`
318+
Port int `yaml:"port" json:"port"`
319+
}

go.mod

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ require (
6363
github.com/golang/snappy v0.0.4 // indirect
6464
github.com/google/go-github/v84 v84.0.0 // indirect
6565
github.com/google/go-querystring v1.2.0 // indirect
66+
github.com/google/jsonschema-go v0.4.2 // indirect
6667
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
6768
github.com/huandu/xstrings v1.3.2 // indirect
6869
github.com/imdario/mergo v0.3.14 // indirect
@@ -71,16 +72,21 @@ require (
7172
github.com/kevinburke/ssh_config v1.2.0 // indirect
7273
github.com/mitchellh/copystructure v1.0.0 // indirect
7374
github.com/mitchellh/reflectwalk v1.0.0 // indirect
75+
github.com/modelcontextprotocol/go-sdk v1.4.1 // indirect
7476
github.com/mschoch/smat v0.2.0 // indirect
7577
github.com/pjbgf/sha1cd v0.3.2 // indirect
7678
github.com/pmezard/go-difflib v1.0.0 // indirect
7779
github.com/robfig/cron/v3 v3.0.1 // indirect
80+
github.com/segmentio/asm v1.1.3 // indirect
81+
github.com/segmentio/encoding v0.5.4 // indirect
7882
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
7983
github.com/skeema/knownhosts v1.3.1 // indirect
8084
github.com/xanzy/ssh-agent v0.3.3 // indirect
85+
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
8186
go.etcd.io/bbolt v1.4.0 // indirect
8287
go.uber.org/atomic v1.9.0 // indirect
8388
golang.org/x/crypto v0.49.0 // indirect
89+
golang.org/x/oauth2 v0.34.0 // indirect
8490
golang.org/x/sys v0.42.0 // indirect
8591
google.golang.org/protobuf v1.36.6 // indirect
8692
gopkg.in/warnings.v0 v0.1.2 // indirect

go.sum

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfh
114114
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
115115
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
116116
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
117+
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
118+
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
117119
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
118120
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
119121
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -144,6 +146,8 @@ github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMK
144146
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
145147
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
146148
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
149+
github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc=
150+
github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s=
147151
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
148152
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
149153
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
@@ -161,6 +165,10 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE
161165
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
162166
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
163167
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
168+
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
169+
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
170+
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
171+
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
164172
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
165173
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
166174
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
@@ -182,6 +190,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
182190
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
183191
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
184192
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
193+
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
194+
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
185195
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
186196
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
187197
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
@@ -197,6 +207,8 @@ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbR
197207
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
198208
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
199209
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
210+
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
211+
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
200212
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
201213
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
202214
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

lib/http.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ func GetUrl(r *http.Request) string {
1515
} else {
1616
sb.WriteString("http://")
1717
}
18-
sb.WriteString(r.Host)
18+
if r.Host != "" {
19+
sb.WriteString(r.Host)
20+
} else {
21+
sb.WriteString("localhost")
22+
}
1923
sb.WriteString(r.URL.String())
2024
return sb.String()
2125
}

mcp/get_api_spec.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package mcp
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/modelcontextprotocol/go-sdk/mcp"
8+
)
9+
10+
type GetSpecInput struct {
11+
Name string `json:"name"`
12+
Type string `json:"type"`
13+
}
14+
15+
func (s *Service) registerGetSpecTool(server *mcp.Server) {
16+
inputSchema := map[string]any{
17+
"type": "object",
18+
"properties": map[string]any{
19+
"name": map[string]any{
20+
"type": "string",
21+
"description": "The exact name of the API as returned by 'get_api_list'",
22+
},
23+
"type": map[string]any{
24+
"type": "string",
25+
"description": "The type of the API as returned by 'get_api_list'",
26+
"enum": []string{"http", "kafka", "ldap", "mail"},
27+
},
28+
},
29+
}
30+
31+
registerTool(server, &mcp.Tool{
32+
Name: "get_api_spec",
33+
Description: `Get the full API specification for a specific API.
34+
35+
This tool should be used AFTER calling 'get_api_list' to find available APIs.
36+
First use 'get_api_list' to discover API names and types, then call this tool with the exact 'name' and 'type'.
37+
38+
Returns the complete specification including endpoints, operations, and schemas.`,
39+
InputSchema: inputSchema,
40+
}, s.GetApiSpec)
41+
}
42+
43+
func (s *Service) GetApiSpec(_ context.Context, in GetSpecInput) (any, error) {
44+
switch in.Type {
45+
case "http":
46+
info := s.app.GetHttp(in.Name)
47+
if info == nil {
48+
return nil, fmt.Errorf("http api spec not found")
49+
}
50+
return info.Config, nil
51+
case "kafka":
52+
info := s.app.Kafka.Get(in.Name)
53+
if info == nil {
54+
return nil, fmt.Errorf("kafka api spec not found")
55+
}
56+
return info.Config, nil
57+
case "ldap":
58+
info := s.app.Ldap.Get(in.Name)
59+
if info == nil {
60+
return nil, fmt.Errorf("ldap api spec not found")
61+
}
62+
return info.Config, nil
63+
case "mail":
64+
info := s.app.Mail.Get(in.Name)
65+
if info == nil {
66+
return nil, fmt.Errorf("mail api spec not found")
67+
}
68+
return info.Config, nil
69+
}
70+
71+
return nil, fmt.Errorf("invalid type: %s", in.Type)
72+
}

mcp/get_events.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package mcp
2+
3+
import (
4+
"context"
5+
"mokapi/runtime/events"
6+
7+
"github.com/modelcontextprotocol/go-sdk/mcp"
8+
)
9+
10+
type GetEventsInput struct {
11+
APIName string `json:"apiName"`
12+
Type string `json:"type"`
13+
Limit *int `json:"limit"`
14+
Traits map[string]string `json:"traits"`
15+
}
16+
17+
type GetEventsResponse struct {
18+
Events []events.Event `json:"events"`
19+
}
20+
21+
func (s *Service) registerGetEvents(server *mcp.Server) {
22+
inputSchema := map[string]any{
23+
"type": "object",
24+
"properties": map[string]any{
25+
"apiName": map[string]any{
26+
"type": "string",
27+
"description": "Filter events by API name",
28+
},
29+
"type": map[string]any{
30+
"type": "string",
31+
"description": "Filter by event type",
32+
"enum": []string{"http", "kafka"},
33+
},
34+
"traits": map[string]any{
35+
"type": "object",
36+
"description": "Filter events by traits",
37+
"additionalProperties": map[string]interface{}{
38+
"type": "string",
39+
},
40+
},
41+
"limit": map[string]any{
42+
"type": "integer",
43+
"description": "Maximum number of events to return",
44+
"default": 10,
45+
},
46+
},
47+
}
48+
49+
outputSchema := map[string]any{
50+
"type": "object",
51+
"properties": map[string]any{
52+
"events": map[string]any{
53+
"type": "array",
54+
"description": "List of events",
55+
"items": map[string]any{
56+
"type": "object",
57+
"properties": map[string]any{
58+
"id": map[string]any{
59+
"type": "string",
60+
"description": "ID of the event",
61+
},
62+
"traits": map[string]any{
63+
"type": "object",
64+
"description": "List of traits",
65+
"additionalProperties": map[string]interface{}{
66+
"type": "string",
67+
},
68+
},
69+
"data": map[string]any{
70+
"type": "object",
71+
"description": "The data of the event",
72+
},
73+
"time": map[string]any{
74+
"type": "string",
75+
"description": "Time of the event",
76+
"format": "date-time",
77+
},
78+
},
79+
},
80+
},
81+
},
82+
}
83+
84+
registerTool(server, &mcp.Tool{
85+
Name: "get_events",
86+
Description: `Returns recorded events from Mokapi including HTTP requests/responses and Kafka messages.
87+
88+
Use this tool when the user asks:
89+
- "What requests were made?"
90+
- "Why did my request fail?"
91+
- "Show recent API activity"
92+
- "What messages were produced to Kafka?"
93+
94+
Each event contains:
95+
- metadata: id, time, traits
96+
- HTTP data: request (method, URL, parameters, body) and response (status, headers, body, duration)
97+
- Kafka data: message payload, key, headers, partition, offset
98+
99+
Call this tool after sending requests or producing messages to inspect results and debug behavior.`,
100+
InputSchema: inputSchema,
101+
OutputSchema: outputSchema,
102+
}, s.ProduceKafkaMessage)
103+
}
104+
105+
func (s *Service) GetEvents(_ context.Context, in GetEventsInput) (GetEventsResponse, error) {
106+
result := GetEventsResponse{}
107+
108+
traits, err := bindInput[events.Traits](in.Traits)
109+
if err != nil {
110+
return result, err
111+
}
112+
if traits == nil {
113+
traits = events.NewTraits()
114+
}
115+
if in.Type != "" {
116+
traits.WithNamespace(in.Type)
117+
}
118+
119+
evts := s.app.Events.GetEvents(traits)
120+
121+
limit := 10
122+
if in.Limit != nil {
123+
limit = *in.Limit
124+
}
125+
if len(evts) > limit {
126+
result.Events = evts[0:limit]
127+
} else {
128+
result.Events = evts
129+
}
130+
return result, nil
131+
}

0 commit comments

Comments
 (0)