4cddfcf2f3
Backend Tests / Tests (other) (push) Failing after 1s
Backend Tests / Static Checks (push) Failing after 2s
Backend Tests / Tests (internal) (push) Failing after 2s
Backend Tests / Tests (server) (push) Failing after 1s
Backend Tests / Tests (store) (push) Failing after 1s
Build Canary Image / build-frontend (push) Failing after 1s
Frontend Tests / Lint (push) Failing after 1s
Build Canary Image / build-push (linux/amd64) (push) Has been skipped
Build Canary Image / build-push (linux/arm64) (push) Has been skipped
Build Canary Image / merge (push) Has been skipped
Frontend Tests / Build (push) Failing after 1s
Release Please / release-please (push) Failing after 0s
Proto Linter / Lint Protos (push) Failing after 2s
161 lines
4.1 KiB
Go
161 lines
4.1 KiB
Go
package mcp
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/labstack/echo/v5"
|
|
sdkmcp "github.com/modelcontextprotocol/go-sdk/mcp"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
type apiAdapter struct {
|
|
echoServer *echo.Echo
|
|
}
|
|
|
|
func newAPIAdapter(echoServer *echo.Echo) *apiAdapter {
|
|
return &apiAdapter{echoServer: echoServer}
|
|
}
|
|
|
|
func (a *apiAdapter) execute(ctx context.Context, operation *openAPIOperation, arguments map[string]any, authorization string) (*sdkmcp.CallToolResult, error) {
|
|
req, err := buildAPIRequest(ctx, operation, arguments, authorization)
|
|
if err != nil {
|
|
return newToolErrorResult(err.Error()), nil
|
|
}
|
|
|
|
recorder := httptest.NewRecorder()
|
|
a.echoServer.ServeHTTP(recorder, req)
|
|
|
|
value, err := decodeJSONValue(recorder.Body.Bytes())
|
|
if err != nil {
|
|
return newToolErrorResult(err.Error()), nil
|
|
}
|
|
if recorder.Code < http.StatusOK || recorder.Code >= http.StatusMultipleChoices {
|
|
return newToolErrorResult(apiErrorMessage(recorder.Code, value)), nil
|
|
}
|
|
return newStructuredToolResult(value)
|
|
}
|
|
|
|
func buildAPIRequest(ctx context.Context, operation *openAPIOperation, arguments map[string]any, authorization string) (*http.Request, error) {
|
|
path, err := substitutePathParameters(operation, arguments)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
query := url.Values{}
|
|
for _, parameter := range operation.Parameters {
|
|
if parameter.In != "query" {
|
|
continue
|
|
}
|
|
value, ok := arguments[parameter.Name]
|
|
if !ok || value == nil {
|
|
continue
|
|
}
|
|
query.Set(parameter.Name, valueToString(value))
|
|
}
|
|
if encoded := query.Encode(); encoded != "" {
|
|
path += "?" + encoded
|
|
}
|
|
|
|
var body io.Reader
|
|
if operation.RequestBody != nil {
|
|
bodyValue, ok := arguments["body"]
|
|
if !ok || bodyValue == nil {
|
|
if operation.RequestBody.Required {
|
|
return nil, errors.New(`missing required request body "body"`)
|
|
}
|
|
bodyValue = map[string]any{}
|
|
}
|
|
|
|
data, err := json.Marshal(bodyValue)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to marshal request body")
|
|
}
|
|
body = bytes.NewReader(data)
|
|
}
|
|
|
|
req := httptest.NewRequest(operation.Method, path, body).WithContext(ctx)
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
if authorization != "" {
|
|
req.Header.Set("Authorization", authorization)
|
|
}
|
|
return req, nil
|
|
}
|
|
|
|
func substitutePathParameters(operation *openAPIOperation, arguments map[string]any) (string, error) {
|
|
path := operation.Path
|
|
for _, parameter := range operation.Parameters {
|
|
if parameter.In != "path" {
|
|
continue
|
|
}
|
|
|
|
value, ok := arguments[parameter.Name]
|
|
if !ok || value == nil || valueToString(value) == "" {
|
|
return "", errors.Errorf(`missing required path parameter "%s"`, parameter.Name)
|
|
}
|
|
path = strings.ReplaceAll(path, "{"+parameter.Name+"}", url.PathEscape(valueToString(value)))
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
func valueToString(value any) string {
|
|
switch typed := value.(type) {
|
|
case string:
|
|
return typed
|
|
case bool:
|
|
return strconv.FormatBool(typed)
|
|
case int:
|
|
return strconv.Itoa(typed)
|
|
case int8:
|
|
return strconv.FormatInt(int64(typed), 10)
|
|
case int16:
|
|
return strconv.FormatInt(int64(typed), 10)
|
|
case int32:
|
|
return strconv.FormatInt(int64(typed), 10)
|
|
case int64:
|
|
return strconv.FormatInt(typed, 10)
|
|
case uint:
|
|
return strconv.FormatUint(uint64(typed), 10)
|
|
case uint8:
|
|
return strconv.FormatUint(uint64(typed), 10)
|
|
case uint16:
|
|
return strconv.FormatUint(uint64(typed), 10)
|
|
case uint32:
|
|
return strconv.FormatUint(uint64(typed), 10)
|
|
case uint64:
|
|
return strconv.FormatUint(typed, 10)
|
|
case float32:
|
|
return strconv.FormatFloat(float64(typed), 'f', -1, 32)
|
|
case float64:
|
|
return strconv.FormatFloat(typed, 'f', -1, 64)
|
|
default:
|
|
data, err := json.Marshal(value)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.Trim(string(data), `"`)
|
|
}
|
|
}
|
|
|
|
func apiErrorMessage(statusCode int, value any) string {
|
|
status := strings.TrimSpace(strconv.Itoa(statusCode) + " " + http.StatusText(statusCode))
|
|
if object, ok := value.(map[string]any); ok {
|
|
for _, key := range []string{"message", "error"} {
|
|
message, ok := object[key].(string)
|
|
if ok && message != "" {
|
|
return status + ": " + message
|
|
}
|
|
}
|
|
}
|
|
return status
|
|
}
|