Skip to content

Commit 94a24bd

Browse files
committed
wip: implement mcp server code mode
1 parent b40be93 commit 94a24bd

13 files changed

Lines changed: 527 additions & 20 deletions

config/dynamic/resolve.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,8 @@ func resolveResource(ref string, element interface{}, config *Config, reader Rea
266266
func setResolved(element interface{}, val interface{}) (err error) {
267267
v := reflect.ValueOf(val)
268268
vElement := reflect.Indirect(reflect.ValueOf(element))
269+
i := vElement.Interface()
270+
_ = i
269271

270272
if v.Kind() == reflect.Ptr {
271273
v = v.Elem()
@@ -303,7 +305,15 @@ func setResolved(element interface{}, val interface{}) (err error) {
303305
return fmt.Errorf("expected type %v, got %v", vElement.Type(), vCursor.Type())
304306
}
305307

308+
n1 := fmt.Sprintf("%s.%s", vCursor.Type().PkgPath(), vCursor.Type().Name())
309+
_ = n1
310+
n2 := fmt.Sprintf("%s.%s", vElement.Type().PkgPath(), vElement.Type().Name())
311+
_ = n2
312+
313+
b := vElement.Type() == vCursor.Type()
314+
_ = b
306315
vElement.Set(vCursor)
316+
i = vElement.Interface()
307317

308318
return
309319
}

mcp/generate_http_mock_response.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func (s *Service) registerGenerateHttpMockResponseTool(server *mcp.Server) {
3232
"properties": map[string]any{
3333
"apiName": map[string]any{
3434
"type": "string",
35-
"description": "The exact name of the API as returned by 'get_api_list'",
35+
"description": "The exact name of the API as returned by 'mokapi_get_api_spec'",
3636
},
3737
"path": map[string]any{
3838
"type": "string",

mcp/get_api_spec.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type GetApiSpecOutput struct {
2020
type ApiSpec struct {
2121
Name string `json:"name"`
2222
Type string `json:"type"`
23-
Spec any `json:"spec"`
23+
Spec any `json:"spec,omitempty"`
2424
}
2525

2626
func (s *Service) registerGetSpecTool(server *mcp.Server) {
@@ -60,10 +60,11 @@ func (s *Service) registerGetSpecTool(server *mcp.Server) {
6060
"enum": []string{"http", "kafka", "ldap", "mail"},
6161
},
6262
"spec": map[string]any{
63-
"type": "any",
63+
"type": "object",
6464
"description": "The specification of the API (e.g. OpenAPI or AsyncAPI",
6565
},
6666
},
67+
"required": []any{"name", "type"},
6768
},
6869
},
6970
},
@@ -96,7 +97,7 @@ func (s *Service) GetApiSpec(_ context.Context, in GetApiSpecInput) (GetApiSpecO
9697
if in.Type == "http" || len(in.Type) == 0 {
9798
for _, api := range s.app.ListHttp() {
9899
if api.Info.Name == "" {
99-
log.Warnf("mcp tool get_api_list: skip empty HTTTP API name")
100+
log.Warnf("mcp tool mokapi_get_api_spec: skip empty HTTTP API name")
100101
continue
101102
}
102103
result = append(result, ApiSpec{
@@ -109,7 +110,7 @@ func (s *Service) GetApiSpec(_ context.Context, in GetApiSpecInput) (GetApiSpecO
109110
if in.Type == "kafka" || len(in.Type) == 0 {
110111
for _, api := range s.app.Kafka.List() {
111112
if api.Info.Name == "" {
112-
log.Warnf("mcp tool get_api_list: skip empty Kafka API name")
113+
log.Warnf("mcp tool mokapi_get_api_spec: skip empty Kafka API name")
113114
continue
114115
}
115116
result = append(result, ApiSpec{
@@ -122,7 +123,7 @@ func (s *Service) GetApiSpec(_ context.Context, in GetApiSpecInput) (GetApiSpecO
122123
if in.Type == "ldap" || len(in.Type) == 0 {
123124
for _, api := range s.app.Ldap.List() {
124125
if api.Info.Name == "" {
125-
log.Warnf("mcp tool get_api_list: skip empty LDAP API name")
126+
log.Warnf("mcp tool mokapi_get_api_spec: skip empty LDAP API name")
126127
continue
127128
}
128129
result = append(result, ApiSpec{
@@ -135,7 +136,7 @@ func (s *Service) GetApiSpec(_ context.Context, in GetApiSpecInput) (GetApiSpecO
135136
if in.Type == "mail" || len(in.Type) == 0 {
136137
for _, api := range s.app.Mail.List() {
137138
if api.Info.Name == "" {
138-
log.Warnf("mcp tool get_api_list: skip empty Mail API name")
139+
log.Warnf("mcp tool mokapi_get_api_spec: skip empty Mail API name")
139140
continue
140141
}
141142
result = append(result, ApiSpec{

mcp/get_http_response_schema.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func (s *Service) registerGetHttpResponseSchemaTool(server *mcp.Server) {
2222
"properties": map[string]any{
2323
"apiName": map[string]any{
2424
"type": "string",
25-
"description": "The exact name of the API as returned by 'get_api_list'",
25+
"description": "The exact name of the API as returned by 'mokapi_get_api_spec'",
2626
},
2727
"path": map[string]any{
2828
"type": "string",

mcp/list_apis.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package mcp
22

3+
/*
34
import (
45
"context"
56
@@ -58,7 +59,7 @@ func (s *Service) registerListApiTool(server *mcp.Server) {
5859
5960
registerTool(server, &mcp.Tool{
6061
Name: "get_api_list",
61-
Description: `Returns all available APIs with their name and type.
62+
Description: `Returns all available APIs with their name and type.
6263
Use this to discover APIs before calling 'get_api_spec' to retrieve detailed specifications.`,
6364
InputSchema: inputSchema,
6465
OutputSchema: outputSchema,
@@ -122,3 +123,4 @@ func (s *Service) ListApis(_ context.Context, in ListApisInput) (*ListApiRespons
122123
123124
return &ListApiResponse{Apis: result}, nil
124125
}
126+
*/

mcp/produce_kafka_message.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func (s *Service) registerProduceKafkaMessage(server *mcp.Server) {
2929
"properties": map[string]any{
3030
"apiName": map[string]any{
3131
"type": "string",
32-
"description": "The name of the Kafka API as returned by 'get_api_list'",
32+
"description": "The name of the Kafka API as returned by 'mokapi_get_api_spec'",
3333
},
3434
"topic": map[string]any{
3535
"type": "string",

mcp/run.go

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
package mcp
2+
3+
import (
4+
"context"
5+
_ "embed"
6+
"mokapi/runtime"
7+
"reflect"
8+
"slices"
9+
"strings"
10+
11+
"github.com/dop251/goja"
12+
"github.com/modelcontextprotocol/go-sdk/mcp"
13+
log "github.com/sirupsen/logrus"
14+
)
15+
16+
//go:embed run.ts
17+
var types string
18+
19+
type RunInput struct {
20+
Code string `json:"code"`
21+
}
22+
23+
type RunOutput struct {
24+
Result any `json:"result"`
25+
}
26+
27+
func (s *Service) registerRunTool(server *mcp.Server) {
28+
inputSchema := map[string]any{
29+
"type": "object",
30+
"properties": map[string]any{
31+
"code": map[string]any{
32+
"type": "string",
33+
"description": "JavaScript code to execute in the Mokapi runtime. The last expression is returned as the result.",
34+
},
35+
},
36+
"required": []string{"code"},
37+
}
38+
39+
outputSchema := map[string]any{
40+
"type": "object",
41+
"properties": map[string]any{
42+
"result": map[string]any{
43+
"description": "The result of the executed code.",
44+
"nullable": true,
45+
},
46+
},
47+
"required": []string{"result"},
48+
}
49+
50+
registerTool(server, &mcp.Tool{
51+
Name: "mokapi_execute_code",
52+
Description: `Executes JavaScript code in a sandboxed Mokapi runtime.
53+
The last expression in the code is returned as the result.
54+
55+
Important:
56+
Before writing any code, be sure to read the API definitions at api://execute-types to understand
57+
the available global objects, functions, and types.
58+
59+
Use this tool to:
60+
- Explore mocked APIs (OpenAPI, AsyncAPI, LDAP, Mail)
61+
- Inspect operations and schemas
62+
- Invoke API operations directly
63+
64+
Prefer this tool over retrieving full API specifications, as it returns only the computed result.`,
65+
InputSchema: inputSchema,
66+
OutputSchema: outputSchema,
67+
}, s.GenerateHttpMockResponse)
68+
69+
server.AddResource(&mcp.Resource{
70+
URI: "api://execute-types",
71+
Name: "api-docs",
72+
}, func(ctx context.Context, request *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
73+
return &mcp.ReadResourceResult{
74+
Contents: []*mcp.ResourceContents{
75+
{
76+
URI: "api://types",
77+
MIMEType: "application/typescript",
78+
Text: types,
79+
},
80+
},
81+
}, nil
82+
})
83+
}
84+
85+
func (s *Service) GetRunResponse(_ context.Context, in RunInput) (RunOutput, error) {
86+
m := newMokapi(s.app)
87+
r, err := m.run(in.Code)
88+
if err != nil {
89+
return RunOutput{}, err
90+
}
91+
92+
return RunOutput{Result: r}, nil
93+
}
94+
95+
type mokapi struct {
96+
app *runtime.App
97+
vm *goja.Runtime
98+
}
99+
100+
func newMokapi(app *runtime.App) *mokapi {
101+
vm := goja.New()
102+
vm.SetFieldNameMapper(&customFieldNameMapper{})
103+
return &mokapi{app: app, vm: vm}
104+
}
105+
106+
func (m *mokapi) run(code string) (any, error) {
107+
obj := m.vm.NewObject()
108+
m.init(obj)
109+
_ = m.vm.Set("mokapi", obj)
110+
v, err := m.vm.RunString(code)
111+
if err != nil {
112+
return nil, err
113+
}
114+
return v.Export(), nil
115+
}
116+
117+
type ApiSummary struct {
118+
Name string `json:"name"`
119+
Type string `json:"type"`
120+
}
121+
122+
func (m *mokapi) init(obj *goja.Object) {
123+
_ = obj.Set("getApis", m.getApis)
124+
_ = obj.Set("getApi", m.getApi)
125+
}
126+
127+
func (m *mokapi) getApis() []ApiSummary {
128+
var result []ApiSummary
129+
for _, api := range m.app.ListHttp() {
130+
if api.Info.Name == "" {
131+
log.Warnf("mcp tool mokapi_get_api_spec: skip empty HTTTP API name")
132+
continue
133+
}
134+
result = append(result, ApiSummary{
135+
Name: api.Info.Name,
136+
Type: "http",
137+
})
138+
}
139+
slices.SortStableFunc(result, func(a, b ApiSummary) int {
140+
return strings.Compare(a.Name, b.Name)
141+
})
142+
return result
143+
}
144+
145+
func (m *mokapi) getApi(name string) any {
146+
for _, api := range m.app.ListHttp() {
147+
if api.Info.Name == name {
148+
return &OpenAPI{
149+
Name: name,
150+
Type: "http",
151+
info: api,
152+
}
153+
}
154+
}
155+
return nil
156+
}
157+
158+
type OpenAPI struct {
159+
Name string `json:"name"`
160+
Type string `json:"type"`
161+
162+
info *runtime.HttpInfo
163+
}
164+
165+
type OperationSummary struct {
166+
Method string `json:"method"`
167+
Path string `json:"path"`
168+
Summary string `json:"summary"`
169+
}
170+
171+
type OperationDetails struct {
172+
OperationId string `json:"operationId"`
173+
Method string `json:"method"`
174+
Path string `json:"path"`
175+
Summary string `json:"summary"`
176+
Description string `json:"description"`
177+
}
178+
179+
func (o *OpenAPI) GetOperations() []OperationSummary {
180+
var result []OperationSummary
181+
for _, p := range o.info.Paths {
182+
if p.Value == nil {
183+
continue
184+
}
185+
for method, op := range p.Value.Operations() {
186+
summary := op.Summary
187+
if summary == "" {
188+
summary = p.Value.Summary
189+
}
190+
result = append(result, OperationSummary{
191+
Method: method,
192+
Path: p.Value.Path,
193+
Summary: summary,
194+
})
195+
}
196+
}
197+
198+
slices.SortStableFunc(result, func(a, b OperationSummary) int {
199+
c := strings.Compare(a.Path, b.Path)
200+
if c != 0 {
201+
return c
202+
}
203+
return strings.Compare(a.Method, b.Method)
204+
})
205+
206+
return result
207+
}
208+
209+
func (o *OpenAPI) GetOperationDetails(path, method string) *OperationDetails {
210+
for _, p := range o.info.Paths {
211+
if p.Value == nil || p.Value.Path != path {
212+
continue
213+
}
214+
op := p.Value.Operation(method)
215+
if op == nil {
216+
continue
217+
}
218+
return &OperationDetails{
219+
OperationId: op.OperationId,
220+
Method: method,
221+
Path: p.Value.Path,
222+
Summary: op.Summary,
223+
Description: op.Description,
224+
}
225+
}
226+
return nil
227+
}
228+
229+
type customFieldNameMapper struct {
230+
}
231+
232+
func (cfm customFieldNameMapper) FieldName(_ reflect.Type, f reflect.StructField) string {
233+
tag := f.Tag.Get("json")
234+
if len(tag) == 0 {
235+
return uncapitalize(f.Name)
236+
}
237+
if idx := strings.IndexByte(tag, ','); idx != -1 {
238+
tag = tag[:idx]
239+
}
240+
241+
return tag
242+
}
243+
244+
func (cfm customFieldNameMapper) MethodName(_ reflect.Type, m reflect.Method) string {
245+
return uncapitalize(m.Name)
246+
}
247+
248+
func uncapitalize(s string) string {
249+
return strings.ToLower(s[0:1]) + s[1:]
250+
}

0 commit comments

Comments
 (0)