Skip to content

Commit d52cf43

Browse files
committed
wip: mcp server execute code
- add Kafka support - refactor run code
1 parent 98f72dc commit d52cf43

15 files changed

Lines changed: 1534 additions & 747 deletions

mcp/conditional-response.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ Demonstrates how to:
55
- Access request parameters
66
- Apply custom logic (e.g., lookup, filtering, updates)
77

8+
IMPORTANT
9+
Strict Specification Enforcement:
10+
Mokapi will throw an error if you use a status code NOT defined in the specification.
11+
Always verify the available status codes for each operation before calling response.rebuild() or setting response.statusCode.
12+
813
```typescript
914
import { on } from "mokapi"
1015

@@ -24,6 +29,7 @@ export default function () {
2429
case '/terminals/{id}': {
2530
const terminal = terminals.find(x => x.id === request.path.id)
2631
if (!terminal) {
32+
// CHECK SPEC: Is 404 defined for this path and HTTP method?
2733
response.rebuild(404)
2834
response.data = { error: 'terminal not found' }
2935
return
@@ -48,6 +54,7 @@ export default function () {
4854
if (terminal) {
4955
// console output will be displayed in the Mokapi's' dashboard
5056
console.log('terminal already exists', request.body)
57+
// CHECK SPEC: Is 400 defined for this path and HTTP method?
5158
response.rebuild(400)
5259
} else {
5360
terminals.push(request.body)

mcp/mock_reference.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ with the corresponding category and name (e.g., category='types', name='http')
1111
Overview of TypeScript definitions for mock event handlers.
1212
- Use these types to ensure correct syntax for "import { ... } from 'mokapi/...'".
1313

14-
| Type | Import Statement | Parameter Name |
15-
|--------------------------------|---------------------------------------|----------------|
16-
| **Core** | `import {...} from 'mokapi'` | mokapi |
17-
| **HTTP** | `import {...} from 'mokapi/http'` | http |
18-
| **Kafka** | `import {...} from 'mokapi/kafka'` | kafka |
19-
| **Faker** | `import {...} from 'mokapi/faker'` | faker |
20-
| **Mustache** | `import {...} from 'mokapi/mustache'` | mustache |
21-
| **Yaml** | `import {...} from 'mokapi/yaml'` | yaml |
14+
| Type | Description | Import Statement | Parameter Name |
15+
|--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------|----------------------|
16+
| **Core** | Exposes the core scripting API for Mokapi. It allows you to intercept and manipulate protocol events (HTTP, Kafka, LDAP, SMTP), schedule jobs, generate mock data, and share state between scripts. | `import {...} from 'mokapi'` | mokapi |
17+
| **HTTP** | Exposes functions to invoke HTTP request | `import {...} from 'mokapi/http'` | http |
18+
| **Kafka** | Exposes functions to produce Kafka message | `import {...} from 'mokapi/kafka'` | kafka |
19+
| **Faker** | Exposes functions to generate random data based on JSON schema | `import {...} from 'mokapi/faker'` | faker |
20+
| **Mustache** | Exposes to render output based on mustache template | `import {...} from 'mokapi/mustache'` | mustache |
21+
| **Yaml** | Exposes functions to parse or stringify YAML files | `import {...} from 'mokapi/yaml'` | yaml |
2222

2323
## Scenarios
2424

mcp/run.go

Lines changed: 17 additions & 288 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,11 @@ import (
55
_ "embed"
66
"errors"
77
"fmt"
8-
"io"
98
"mokapi/js/compiler"
109
"mokapi/js/faker"
1110
"mokapi/providers/openapi"
12-
"mokapi/providers/openapi/schema"
1311
"mokapi/runtime"
1412
"mokapi/schema/json/generator"
15-
"net/http"
16-
"net/textproto"
1713
"reflect"
1814
"slices"
1915
"strings"
@@ -137,32 +133,38 @@ func (m *mokapi) getApis() []ApiSummary {
137133
var result []ApiSummary
138134
for _, api := range m.app.ListHttp() {
139135
if api.Info.Name == "" {
140-
log.Warnf("mcp tool mokapi_get_api_spec: skip empty HTTTP API name")
136+
log.Warnf("mcp tool mokapi_execute_code: skip empty HTTTP API name")
141137
continue
142138
}
143139
result = append(result, ApiSummary{
144140
Name: api.Info.Name,
145141
Type: "http",
146142
})
147143
}
144+
for _, api := range m.app.Kafka.List() {
145+
if api.Info.Name == "" {
146+
log.Warnf("mcp tool mokapi_execute_code: skip empty Kafka API name")
147+
continue
148+
}
149+
result = append(result, ApiSummary{
150+
Name: api.Info.Name,
151+
Type: "kafka",
152+
})
153+
}
148154
slices.SortStableFunc(result, func(a, b ApiSummary) int {
149155
return strings.Compare(a.Name, b.Name)
150156
})
151157
return result
152158
}
153159

154160
func (m *mokapi) getApi(name string) any {
155-
for _, api := range m.app.ListHttp() {
156-
if api.Info.Name == name {
157-
return &OpenAPI{
158-
Name: name,
159-
Type: "http",
160-
info: api,
161-
handler: api.Handler(m.app.Monitor.Http, m.app.Engine, m.app.Events),
162-
}
163-
}
161+
var api any
162+
api = m.getHttpApi(name)
163+
if api != nil {
164+
return api
164165
}
165-
return nil
166+
api = m.getKafkaApi(name)
167+
return api
166168
}
167169

168170
func (m *mokapi) fake(v goja.Value) (any, error) {
@@ -173,279 +175,6 @@ func (m *mokapi) fake(v goja.Value) (any, error) {
173175
return generator.New(&generator.Request{Schema: js})
174176
}
175177

176-
type OpenAPI struct {
177-
Name string `json:"name"`
178-
Type string `json:"type"`
179-
180-
info *runtime.HttpInfo
181-
handler openapi.Handler
182-
}
183-
184-
type OperationSummary struct {
185-
Id string `json:"id"`
186-
Method string `json:"method"`
187-
Path string `json:"path"`
188-
Summary string `json:"summary"`
189-
Parameters []string `json:"parameters"`
190-
}
191-
192-
type Operation struct {
193-
OperationId string `json:"operationId"`
194-
Method string `json:"method"`
195-
Path string `json:"path"`
196-
Summary string `json:"summary"`
197-
Description string `json:"description,omitempty"`
198-
Parameters []RequestParameters `json:"parameters,omitempty"`
199-
RequestBody RequestBody `json:"requestBody,omitempty"`
200-
201-
spec *openapi.Operation
202-
handler openapi.Handler
203-
}
204-
205-
type RequestParameters struct {
206-
Name string `json:"name"`
207-
In string `json:"in"`
208-
Required bool `json:"required"`
209-
Schema *schema.Schema
210-
Description string `json:"description,omitempty"`
211-
}
212-
213-
type RequestBody struct {
214-
Description string `json:"description,omitempty"`
215-
Required bool `json:"required"`
216-
Contents []Content `json:"contents"`
217-
}
218-
219-
type Content struct {
220-
ContentType string `json:"contentType"`
221-
Schema *schema.Schema `json:"schema"`
222-
}
223-
224-
type Response struct {
225-
StatusCode int `json:"statusCode"`
226-
Description string `json:"description,omitempty"`
227-
Content []Content `json:"content"`
228-
}
229-
230-
func (o *OpenAPI) GetOperations() []OperationSummary {
231-
var result []OperationSummary
232-
for _, p := range o.info.Paths {
233-
if p.Value == nil {
234-
continue
235-
}
236-
for method, op := range p.Value.Operations() {
237-
os := OperationSummary{
238-
Id: getOperationId(method, op),
239-
Method: method,
240-
Path: p.Value.Path,
241-
Summary: op.Summary,
242-
}
243-
244-
if os.Summary == "" {
245-
os.Summary = p.Value.Summary
246-
}
247-
248-
params := append(op.Path.Parameters, op.Parameters...)
249-
for _, param := range params {
250-
if param.Value == nil {
251-
continue
252-
}
253-
os.Parameters = append(os.Parameters, param.Value.Name)
254-
}
255-
slices.SortStableFunc(os.Parameters, func(a, b string) int {
256-
return strings.Compare(a, b)
257-
})
258-
259-
result = append(result, os)
260-
}
261-
}
262-
263-
slices.SortStableFunc(result, func(a, b OperationSummary) int {
264-
c := strings.Compare(a.Path, b.Path)
265-
if c != 0 {
266-
return c
267-
}
268-
return strings.Compare(a.Method, b.Method)
269-
})
270-
271-
return result
272-
}
273-
274-
func (o *OpenAPI) GetOperation(id string) (*Operation, error) {
275-
for _, p := range o.info.Paths {
276-
if p.Value == nil {
277-
continue
278-
}
279-
for method, op := range p.Value.Operations() {
280-
281-
operationId := getOperationId(method, op)
282-
if id != operationId {
283-
continue
284-
}
285-
286-
r := &Operation{
287-
OperationId: operationId,
288-
Method: method,
289-
Path: p.Value.Path,
290-
Summary: op.Summary,
291-
Description: op.Description,
292-
spec: op,
293-
handler: o.handler,
294-
}
295-
for _, param := range op.Parameters {
296-
if param.Value == nil {
297-
continue
298-
}
299-
r.Parameters = append(r.Parameters, RequestParameters{
300-
Name: param.Value.Name,
301-
In: param.Value.Type.String(),
302-
Required: param.Value.Required,
303-
Schema: param.Value.Schema,
304-
Description: param.Value.Description,
305-
})
306-
}
307-
slices.SortStableFunc(r.Parameters, func(a, b RequestParameters) int {
308-
return strings.Compare(a.Name, b.Name)
309-
})
310-
311-
if op.RequestBody != nil && op.RequestBody.Value != nil {
312-
r.RequestBody = RequestBody{
313-
Description: op.RequestBody.Value.Description,
314-
Required: op.RequestBody.Value.Required,
315-
}
316-
for ct, content := range op.RequestBody.Value.Content {
317-
r.RequestBody.Contents = append(r.RequestBody.Contents, Content{
318-
ContentType: ct,
319-
Schema: content.Schema,
320-
})
321-
}
322-
}
323-
return r, nil
324-
}
325-
}
326-
return nil, fmt.Errorf("operation with ID '%s' not found. Hint: Use getOperations() to see the full list of valid IDs", id)
327-
}
328-
329-
func (op *Operation) GetResponseSchema(statusCode int) *Response {
330-
r := op.spec.Responses.GetResponse(statusCode)
331-
if r == nil {
332-
return nil
333-
}
334-
result := &Response{
335-
StatusCode: statusCode,
336-
Description: r.Description,
337-
}
338-
for ct, content := range r.Content {
339-
result.Content = append(result.Content, Content{
340-
ContentType: ct,
341-
Schema: content.Schema,
342-
})
343-
}
344-
return result
345-
}
346-
347-
type InvokeRequest struct {
348-
Path map[string]string `json:"path"`
349-
Query map[string]string `json:"query"`
350-
Header map[string][]string `json:"header"`
351-
Body string `json:"body"`
352-
}
353-
354-
type InvokeResponse struct {
355-
StatusCode int `json:"statusCode"`
356-
Headers map[string][]string `json:"headers"`
357-
Body string `json:"body"`
358-
}
359-
360-
func (op *Operation) Invoke(req InvokeRequest) (InvokeResponse, error) {
361-
result := InvokeResponse{Headers: make(map[string][]string)}
362-
363-
var body io.Reader
364-
if req.Body != "" {
365-
body = strings.NewReader(req.Body)
366-
}
367-
368-
path := op.Path
369-
query := ""
370-
params := append(op.spec.Path.Parameters, op.spec.Parameters...)
371-
for _, p := range params {
372-
if p.Value == nil {
373-
continue
374-
}
375-
switch p.Value.Type {
376-
case openapi.ParameterPath:
377-
if req.Path == nil {
378-
return result, fmt.Errorf("invoke request %s %s failed: missing path parameter '%s'", op.Method, op.Path, p.Value.Name)
379-
}
380-
val, ok := req.Path[p.Value.Name]
381-
if !ok {
382-
return result, fmt.Errorf("invoke request %s %s failed: missing path parameter '%s'", op.Method, op.Path, p.Value.Name)
383-
}
384-
path = strings.ReplaceAll(path, fmt.Sprintf("{%s}", p.Value.Name), val)
385-
case openapi.ParameterQuery:
386-
if req.Query == nil && p.Value.Required {
387-
return result, fmt.Errorf("invoke request %s %s failed: missing query parameter '%s'", op.Method, op.Path, p.Value.Name)
388-
}
389-
val, ok := req.Query[p.Value.Name]
390-
if !ok {
391-
if !p.Value.Required {
392-
continue
393-
}
394-
return result, fmt.Errorf("invoke request %s %s failed: missing query parameter '%s'", op.Method, op.Path, p.Value.Name)
395-
}
396-
if query != "" {
397-
query += "&"
398-
}
399-
query += fmt.Sprintf("%s=%s", p.Value.Name, val)
400-
}
401-
}
402-
403-
if query != "" {
404-
path += "?" + query
405-
}
406-
407-
r, err := http.NewRequest(op.Method, path, body)
408-
if err != nil {
409-
return result, fmt.Errorf("error creating request: %w", err)
410-
}
411-
for _, p := range params {
412-
if p.Value == nil || p.Value.Type != openapi.ParameterHeader {
413-
continue
414-
}
415-
if req.Header == nil && p.Value.Required {
416-
return result, fmt.Errorf("invoke request %s %s failed: missing header parameter '%s'", op.Method, op.Path, p.Value.Name)
417-
}
418-
val, ok := req.Header[p.Value.Name]
419-
if !ok {
420-
if !p.Value.Required {
421-
continue
422-
}
423-
return result, fmt.Errorf("invoke request %s %s failed: missing header parameter '%s'", op.Method, op.Path, p.Value.Name)
424-
}
425-
r.Header[textproto.CanonicalMIMEHeaderKey(p.Value.Name)] = val
426-
}
427-
428-
he := op.handler.ServeHTTP(&result, r)
429-
if he != nil {
430-
result.StatusCode = he.StatusCode
431-
result.Body = he.Message
432-
}
433-
return result, nil
434-
}
435-
436-
func (r *InvokeResponse) Header() http.Header {
437-
return r.Headers
438-
}
439-
440-
func (r *InvokeResponse) WriteHeader(statusCode int) {
441-
r.StatusCode = statusCode
442-
}
443-
444-
func (r *InvokeResponse) Write(body []byte) (int, error) {
445-
r.Body = string(body)
446-
return len(body), nil
447-
}
448-
449178
type customFieldNameMapper struct {
450179
}
451180

0 commit comments

Comments
 (0)