Skip to content

Commit e566800

Browse files
committed
feat(mcp): add tools get_http_response_schema and generate_http_response
1 parent 67a5a4a commit e566800

7 files changed

Lines changed: 544 additions & 109 deletions

mcp/generate_http_response.go

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
package mcp
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"mokapi/media"
7+
"mokapi/providers/openapi"
8+
"mokapi/providers/openapi/schema"
9+
"mokapi/schema/json/generator"
10+
"strings"
11+
12+
"github.com/modelcontextprotocol/go-sdk/mcp"
13+
)
14+
15+
type GenerateHttpResponseInput struct {
16+
ApiName string `json:"apiName"`
17+
Path string `json:"path"`
18+
Method string `json:"method"`
19+
StatusCode int `json:"statusCode"`
20+
ContentType string `json:"contentType,omitempty"`
21+
}
22+
23+
type GenerateHttpResponseOutput struct {
24+
StatusCode int `json:"statusCode"`
25+
Data any `json:"data"`
26+
Headers map[string]any `json:"headers"`
27+
}
28+
29+
func (s *Service) registerGenerateHttpResponseTool(server *mcp.Server) {
30+
inputSchema := map[string]any{
31+
"type": "object",
32+
"properties": map[string]any{
33+
"apiName": map[string]any{
34+
"type": "string",
35+
"description": "The exact name of the API as returned by 'get_api_list'",
36+
},
37+
"path": map[string]any{
38+
"type": "string",
39+
"description": "The path template of the endpoint (e.g. /pets/{id})",
40+
},
41+
"method": map[string]any{
42+
"type": "string",
43+
"description": "The HTTP method (GET, POST, PUT, DELETE, etc.)",
44+
},
45+
"statusCode": map[string]any{
46+
"type": "integer",
47+
"description": "The HTTP status code to generate the response for",
48+
},
49+
"contentType": map[string]any{
50+
"type": "string",
51+
"description": `The HTTP content type of the response body. Optional:
52+
If provided, this content type is used.
53+
If the endpoint has only one content type, it will be used automatically.
54+
Otherwise defaults to 'application/json'`,
55+
"default": "application/json",
56+
},
57+
},
58+
}
59+
60+
outputSchema := map[string]any{
61+
"type": "object",
62+
"properties": map[string]any{
63+
"statusCode": map[string]any{
64+
"type": "integer",
65+
"description": "HTTP status code for the response",
66+
},
67+
"headers": map[string]any{
68+
"type": "object",
69+
"description": "response headers defined by the API specification",
70+
},
71+
"data": map[string]any{
72+
"type": "any",
73+
"description": "structured response body that matches the OpenAPI schema",
74+
},
75+
},
76+
}
77+
78+
registerTool(server, &mcp.Tool{
79+
Name: "generate_http_response",
80+
Description: `Generate a valid HTTP response for a specific API endpoint.
81+
82+
This tool returns a complete response object that already conforms to the OpenAPI specification.
83+
The generated data strictly matches the response schema, including all required fields and correct types.
84+
85+
Use this tool when writing HTTP mock scripts instead of manually constructing response bodies.
86+
87+
The returned object can be used directly in the mock script:
88+
89+
on('http', (request) => {
90+
return GENERATED_RESPONSE
91+
})
92+
93+
The "data" field is preferred and will be automatically encoded based on the API specification.
94+
The "body" field is not returned by this tool and should only be used for raw responses.
95+
96+
Rules:
97+
- Do NOT manually construct complex response objects
98+
- Always prefer this tool to ensure schema-correct responses
99+
- The "data" field contains structured data and will be encoded automatically
100+
- The "statusCode" and "headers" are already set correctly
101+
102+
Example:
103+
Generated response:
104+
{
105+
"statusCode": 200,
106+
"headers": {
107+
"Content-Type": "application/json"
108+
},
109+
"data": {
110+
"id": 1,
111+
"name": "dog",
112+
"status": "available"
113+
}
114+
}
115+
116+
Usage in mock script:
117+
on('http', (request, response) => {
118+
response.statusCode = 200
119+
response.headers["Content-Type"] = "application/json"
120+
response.data = {
121+
"id": 1,
122+
"name": "dog",
123+
"status": "available"
124+
}
125+
})
126+
`,
127+
InputSchema: inputSchema,
128+
OutputSchema: outputSchema,
129+
}, s.GetHttpResponseSchema)
130+
}
131+
132+
func (s *Service) GenerateHttpResponse(_ context.Context, in GenerateHttpResponseInput) (GenerateHttpResponseOutput, error) {
133+
result := GenerateHttpResponseOutput{StatusCode: in.StatusCode, Headers: make(map[string]any)}
134+
135+
info := s.app.GetHttp(in.ApiName)
136+
if info == nil {
137+
return result, fmt.Errorf("http api not found")
138+
}
139+
p, ok := info.Paths[in.Path]
140+
if !ok || p.Value == nil {
141+
return result, fmt.Errorf("path not found")
142+
}
143+
o := p.Value.Operation(in.Method)
144+
if o == nil {
145+
return result, fmt.Errorf("operation not found")
146+
}
147+
r := o.Responses.GetResponse(in.StatusCode)
148+
if r == nil {
149+
return result, fmt.Errorf("response not found")
150+
}
151+
152+
n := len(r.Content)
153+
if n == 0 {
154+
return result, fmt.Errorf("response has no content")
155+
}
156+
157+
var mt *openapi.MediaType
158+
if n == 1 && in.ContentType == "" {
159+
for _, v := range r.Content {
160+
mt = v
161+
break
162+
}
163+
} else {
164+
contentType := "application/json"
165+
if in.ContentType != "" {
166+
contentType = in.ContentType
167+
}
168+
accept := media.ParseContentType(contentType)
169+
for k, v := range r.Content {
170+
key := media.ParseContentType(k)
171+
if accept.Match(key) {
172+
mt = v
173+
break
174+
}
175+
}
176+
}
177+
178+
if mt == nil {
179+
return result, fmt.Errorf("response not found")
180+
}
181+
182+
segments := strings.Split(p.Value.Path, "/")
183+
var names []string
184+
for _, seg := range segments[1:] {
185+
if !strings.HasPrefix(seg, "{") {
186+
names = append(names, seg)
187+
}
188+
}
189+
req := generator.NewRequest(
190+
names,
191+
schema.ConvertToJsonSchema(mt.Schema),
192+
nil,
193+
)
194+
195+
var err error
196+
result.Data, err = generator.New(req)
197+
if err != nil {
198+
return result, err
199+
}
200+
201+
return result, nil
202+
}

mcp/generate_http_response_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package mcp_test
2+
3+
import (
4+
"context"
5+
"mokapi/mcp"
6+
"mokapi/providers/openapi/openapitest"
7+
"mokapi/providers/openapi/schema/schematest"
8+
"mokapi/runtime"
9+
"mokapi/runtime/runtimetest"
10+
"mokapi/schema/json/generator"
11+
"net/http"
12+
"testing"
13+
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func TestService_GenerateHttpResponse(t *testing.T) {
18+
testcases := []struct {
19+
name string
20+
app *runtime.App
21+
test func(t *testing.T, s *mcp.Service)
22+
}{
23+
{
24+
name: "Generate response result",
25+
app: runtimetest.NewApp(
26+
runtimetest.WithHttp(openapitest.NewConfig("3.1.0",
27+
openapitest.WithInfo("foo", "", ""),
28+
openapitest.WithPath("/foo",
29+
openapitest.WithOperation(http.MethodGet,
30+
openapitest.WithResponse(http.StatusOK,
31+
openapitest.WithContent("application/json",
32+
openapitest.WithSchema(schematest.New("string")),
33+
),
34+
),
35+
),
36+
),
37+
)),
38+
),
39+
test: func(t *testing.T, s *mcp.Service) {
40+
result, err := s.GenerateHttpResponse(context.Background(), mcp.GenerateHttpResponseInput{
41+
ApiName: "foo",
42+
Path: "/foo",
43+
Method: http.MethodGet,
44+
StatusCode: http.StatusOK,
45+
})
46+
require.NoError(t, err)
47+
require.Equal(t, mcp.GenerateHttpResponseOutput{
48+
StatusCode: http.StatusOK,
49+
Data: "Ln8rnaRqlL",
50+
Headers: map[string]any{},
51+
}, result)
52+
},
53+
},
54+
}
55+
56+
for _, tc := range testcases {
57+
t.Run(tc.name, func(t *testing.T) {
58+
generator.Seed(12345)
59+
60+
s := mcp.NewService(tc.app)
61+
tc.test(t, s)
62+
})
63+
}
64+
}

mcp/get_http_response_schema.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package mcp
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"mokapi/media"
7+
8+
"github.com/modelcontextprotocol/go-sdk/mcp"
9+
)
10+
11+
type GetHttpResponseSchemaInput struct {
12+
ApiName string `json:"apiName"`
13+
Path string `json:"path"`
14+
Method string `json:"method"`
15+
StatusCode int `json:"statusCode"`
16+
ContentType string `json:"contentType,omitempty"`
17+
}
18+
19+
func (s *Service) registerGetHttpResponseSchemaTool(server *mcp.Server) {
20+
inputSchema := map[string]any{
21+
"type": "object",
22+
"properties": map[string]any{
23+
"apiName": map[string]any{
24+
"type": "string",
25+
"description": "The exact name of the API as returned by 'get_api_list'",
26+
},
27+
"path": map[string]any{
28+
"type": "string",
29+
"description": "The path template of the endpoint (e.g. /pets/{id})",
30+
},
31+
"method": map[string]any{
32+
"type": "string",
33+
"description": "The HTTP method (GET, POST, PUT, DELETE, etc.)",
34+
},
35+
"statusCode": map[string]any{
36+
"type": "integer",
37+
"description": "The HTTP status code",
38+
},
39+
"contentType": map[string]any{
40+
"type": "string",
41+
"description": `The HTTP content type of the response body. Optional:
42+
If provided, this content type is used.
43+
If the endpoint has only one content type, it will be used automatically.
44+
Otherwise defaults to 'application/json'`,
45+
"default": "application/json",
46+
},
47+
},
48+
}
49+
50+
registerTool(server, &mcp.Tool{
51+
Name: "get_http_response_schema",
52+
Description: `Get the HTTP response body schema for a specific API endpoint.
53+
54+
Use this tool **before generating any HTTP mock script**.
55+
The returned schema defines all required fields, types, and nested structures.
56+
All mock responses must strictly conform to this schema. Do not omit required fields or invent extra ones.
57+
`,
58+
InputSchema: inputSchema,
59+
}, s.GetHttpResponseSchema)
60+
}
61+
62+
func (s *Service) GetHttpResponseSchema(_ context.Context, in GetHttpResponseSchemaInput) (any, error) {
63+
info := s.app.GetHttp(in.ApiName)
64+
if info == nil {
65+
return nil, fmt.Errorf("http api not found")
66+
}
67+
p, ok := info.Paths[in.Path]
68+
if !ok || p.Value == nil {
69+
return nil, fmt.Errorf("path not found")
70+
}
71+
o := p.Value.Operation(in.Method)
72+
if o == nil {
73+
return nil, fmt.Errorf("operation not found")
74+
}
75+
r := o.Responses.GetResponse(in.StatusCode)
76+
if r == nil {
77+
return nil, fmt.Errorf("response not found")
78+
}
79+
80+
n := len(r.Content)
81+
if n == 0 {
82+
return nil, fmt.Errorf("response has no content")
83+
}
84+
if n == 1 && in.ContentType == "" {
85+
for _, v := range r.Content {
86+
return v.Schema, nil
87+
}
88+
}
89+
contentType := "application/json"
90+
if in.ContentType != "" {
91+
contentType = in.ContentType
92+
}
93+
mt := media.ParseContentType(contentType)
94+
for k, v := range r.Content {
95+
key := media.ParseContentType(k)
96+
if mt.Match(key) {
97+
return v.Schema, nil
98+
}
99+
}
100+
101+
return nil, fmt.Errorf("content type not found")
102+
}

0 commit comments

Comments
 (0)