mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
feat: skills service
This commit is contained in:
@@ -101,6 +101,9 @@ func (h *ContainerdHandler) Register(e *echo.Echo) {
|
||||
group.DELETE("/containers/:id", h.DeleteContainer)
|
||||
group.POST("/snapshots", h.CreateSnapshot)
|
||||
group.GET("/snapshots", h.ListSnapshots)
|
||||
group.GET("/skills", h.ListSkills)
|
||||
group.POST("/skills", h.UpsertSkills)
|
||||
group.DELETE("/skills", h.DeleteSkills)
|
||||
group.POST("/fs/:id", h.HandleMCPFS)
|
||||
}
|
||||
|
||||
@@ -155,6 +158,9 @@ func (h *ContainerdHandler) CreateContainer(c echo.Context) error {
|
||||
if err := os.MkdirAll(dataDir, 0o755); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(dataDir, ".skills"), 0o755); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
specOpts := []oci.SpecOpts{
|
||||
oci.WithMounts([]specs.Mount{{
|
||||
@@ -241,6 +247,33 @@ func (h *ContainerdHandler) ensureTaskRunning(ctx context.Context, containerID s
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *ContainerdHandler) userContainerID(ctx context.Context, userID string) (string, error) {
|
||||
containers, err := h.service.ListContainersByLabel(ctx, mcp.UserLabelKey, userID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(containers) == 0 {
|
||||
return "", echo.NewHTTPError(http.StatusNotFound, "container not found")
|
||||
}
|
||||
infoCtx := ctx
|
||||
if strings.TrimSpace(h.namespace) != "" {
|
||||
infoCtx = namespaces.WithNamespace(ctx, h.namespace)
|
||||
}
|
||||
bestID := ""
|
||||
var bestUpdated time.Time
|
||||
for _, container := range containers {
|
||||
info, err := container.Info(infoCtx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if bestID == "" || info.UpdatedAt.After(bestUpdated) {
|
||||
bestID = info.ID
|
||||
bestUpdated = info.UpdatedAt
|
||||
}
|
||||
}
|
||||
return bestID, nil
|
||||
}
|
||||
|
||||
// ListContainers godoc
|
||||
// @Summary List containers
|
||||
// @Tags containerd
|
||||
|
||||
+14
-33
@@ -23,25 +23,6 @@ import (
|
||||
mcptools "github.com/memohai/memoh/internal/mcp"
|
||||
)
|
||||
|
||||
type mcpJSONRPCRequest struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID json.RawMessage `json:"id"`
|
||||
Method string `json:"method"`
|
||||
Params json.RawMessage `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
type mcpJSONRPCResponse struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID json.RawMessage `json:"id,omitempty"`
|
||||
Result any `json:"result,omitempty"`
|
||||
Error *mcpJSONRPCError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type mcpJSONRPCError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// HandleMCPFS godoc
|
||||
// @Summary MCP filesystem tools (JSON-RPC)
|
||||
// @Description Forwards MCP JSON-RPC requests to the MCP server inside the container.
|
||||
@@ -72,15 +53,15 @@ func (h *ContainerdHandler) HandleMCPFS(c echo.Context) error {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "container id is required")
|
||||
}
|
||||
|
||||
var req mcpJSONRPCRequest
|
||||
var req mcptools.JSONRPCRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if req.JSONRPC != "" && req.JSONRPC != "2.0" {
|
||||
return c.JSON(http.StatusOK, mcpJSONRPCResponse{
|
||||
return c.JSON(http.StatusOK, mcptools.JSONRPCResponse{
|
||||
JSONRPC: "2.0",
|
||||
ID: req.ID,
|
||||
Error: &mcpJSONRPCError{Code: -32600, Message: "invalid jsonrpc version"},
|
||||
Error: &mcptools.JSONRPCError{Code: -32600, Message: "invalid jsonrpc version"},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -109,10 +90,10 @@ func (h *ContainerdHandler) HandleMCPFS(c echo.Context) error {
|
||||
}
|
||||
return c.JSON(http.StatusOK, payload)
|
||||
default:
|
||||
return c.JSON(http.StatusOK, mcpJSONRPCResponse{
|
||||
return c.JSON(http.StatusOK, mcptools.JSONRPCResponse{
|
||||
JSONRPC: "2.0",
|
||||
ID: req.ID,
|
||||
Error: &mcpJSONRPCError{Code: -32601, Message: "method not found"},
|
||||
Error: &mcptools.JSONRPCError{Code: -32601, Message: "method not found"},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -155,7 +136,7 @@ func (h *ContainerdHandler) requireUserID(c echo.Context) (string, error) {
|
||||
return userID, nil
|
||||
}
|
||||
|
||||
func (h *ContainerdHandler) callMCPServer(ctx context.Context, containerID string, req mcpJSONRPCRequest) (map[string]any, error) {
|
||||
func (h *ContainerdHandler) callMCPServer(ctx context.Context, containerID string, req mcptools.JSONRPCRequest) (map[string]any, error) {
|
||||
session, err := h.getMCPSession(ctx, containerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -171,7 +152,7 @@ type mcpSession struct {
|
||||
initOnce sync.Once
|
||||
writeMu sync.Mutex
|
||||
pendingMu sync.Mutex
|
||||
pending map[string]chan mcpJSONRPCResponse
|
||||
pending map[string]chan mcptools.JSONRPCResponse
|
||||
closed chan struct{}
|
||||
closeOnce sync.Once
|
||||
closeErr error
|
||||
@@ -225,7 +206,7 @@ func (h *ContainerdHandler) startContainerdMCPSession(ctx context.Context, conta
|
||||
stdin: execSession.Stdin,
|
||||
stdout: execSession.Stdout,
|
||||
stderr: execSession.Stderr,
|
||||
pending: make(map[string]chan mcpJSONRPCResponse),
|
||||
pending: make(map[string]chan mcptools.JSONRPCResponse),
|
||||
closed: make(chan struct{}),
|
||||
}
|
||||
|
||||
@@ -290,7 +271,7 @@ func (h *ContainerdHandler) startLimaMCPSession(containerID string) (*mcpSession
|
||||
stdout: stdout,
|
||||
stderr: stderr,
|
||||
cmd: cmd,
|
||||
pending: make(map[string]chan mcpJSONRPCResponse),
|
||||
pending: make(map[string]chan mcptools.JSONRPCResponse),
|
||||
closed: make(chan struct{}),
|
||||
}
|
||||
|
||||
@@ -314,7 +295,7 @@ func (s *mcpSession) closeWithError(err error) {
|
||||
for _, ch := range s.pending {
|
||||
close(ch)
|
||||
}
|
||||
s.pending = map[string]chan mcpJSONRPCResponse{}
|
||||
s.pending = map[string]chan mcptools.JSONRPCResponse{}
|
||||
s.pendingMu.Unlock()
|
||||
_ = s.stdin.Close()
|
||||
_ = s.stdout.Close()
|
||||
@@ -336,7 +317,7 @@ func (s *mcpSession) readLoop() {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
var resp mcpJSONRPCResponse
|
||||
var resp mcptools.JSONRPCResponse
|
||||
if err := json.Unmarshal([]byte(line), &resp); err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -362,7 +343,7 @@ func (s *mcpSession) readLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *mcpSession) call(ctx context.Context, req mcpJSONRPCRequest) (map[string]any, error) {
|
||||
func (s *mcpSession) call(ctx context.Context, req mcptools.JSONRPCRequest) (map[string]any, error) {
|
||||
payloads, targetID, err := buildMCPPayloads(req, &s.initOnce)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -372,7 +353,7 @@ func (s *mcpSession) call(ctx context.Context, req mcpJSONRPCRequest) (map[strin
|
||||
return nil, fmt.Errorf("missing request id")
|
||||
}
|
||||
|
||||
respCh := make(chan mcpJSONRPCResponse, 1)
|
||||
respCh := make(chan mcptools.JSONRPCResponse, 1)
|
||||
s.pendingMu.Lock()
|
||||
s.pending[target] = respCh
|
||||
s.pendingMu.Unlock()
|
||||
@@ -419,7 +400,7 @@ func (s *mcpSession) call(ctx context.Context, req mcpJSONRPCRequest) (map[strin
|
||||
}
|
||||
}
|
||||
|
||||
func buildMCPPayloads(req mcpJSONRPCRequest, initOnce *sync.Once) ([]string, json.RawMessage, error) {
|
||||
func buildMCPPayloads(req mcptools.JSONRPCRequest, initOnce *sync.Once) ([]string, json.RawMessage, error) {
|
||||
if req.JSONRPC == "" {
|
||||
req.JSONRPC = "2.0"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/memohai/memoh/internal/config"
|
||||
mcptools "github.com/memohai/memoh/internal/mcp"
|
||||
)
|
||||
|
||||
type SkillItem struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type SkillsResponse struct {
|
||||
Skills []SkillItem `json:"skills"`
|
||||
}
|
||||
|
||||
type SkillsUpsertRequest struct {
|
||||
Skills []SkillItem `json:"skills"`
|
||||
}
|
||||
|
||||
type SkillsDeleteRequest struct {
|
||||
Names []string `json:"names"`
|
||||
}
|
||||
|
||||
type skillsOpResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
}
|
||||
|
||||
// ListSkills godoc
|
||||
// @Summary List skills from container
|
||||
// @Tags containerd
|
||||
// @Success 200 {object} SkillsResponse
|
||||
// @Failure 400 {object} ErrorResponse
|
||||
// @Failure 404 {object} ErrorResponse
|
||||
// @Failure 500 {object} ErrorResponse
|
||||
// @Router /mcp/skills [get]
|
||||
func (h *ContainerdHandler) ListSkills(c echo.Context) error {
|
||||
userID, err := h.requireUserID(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
containerID, err := h.userContainerID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.ensureTaskRunning(ctx, containerID); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if err := h.ensureSkillsDirHost(userID); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
listPayload, err := h.callMCPTool(ctx, containerID, "fs.list", map[string]any{
|
||||
"path": ".skills",
|
||||
"recursive": false,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
entries, err := extractListEntries(listPayload)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
skills := make([]SkillItem, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
skillPath, name := skillPathForEntry(entry)
|
||||
if skillPath == "" {
|
||||
continue
|
||||
}
|
||||
content, err := h.readSkillFile(ctx, containerID, skillPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
skills = append(skills, SkillItem{
|
||||
Name: name,
|
||||
Description: skillDescription(content),
|
||||
Content: content,
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, SkillsResponse{Skills: skills})
|
||||
}
|
||||
|
||||
// UpsertSkills godoc
|
||||
// @Summary Upload skills into container
|
||||
// @Tags containerd
|
||||
// @Param payload body SkillsUpsertRequest true "Skills payload"
|
||||
// @Success 200 {object} skillsOpResponse
|
||||
// @Failure 400 {object} ErrorResponse
|
||||
// @Failure 404 {object} ErrorResponse
|
||||
// @Failure 500 {object} ErrorResponse
|
||||
// @Router /mcp/skills [post]
|
||||
func (h *ContainerdHandler) UpsertSkills(c echo.Context) error {
|
||||
userID, err := h.requireUserID(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var req SkillsUpsertRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if len(req.Skills) == 0 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "skills is required")
|
||||
}
|
||||
|
||||
ctx := c.Request().Context()
|
||||
containerID, err := h.userContainerID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.ensureTaskRunning(ctx, containerID); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
for _, skill := range req.Skills {
|
||||
name := strings.TrimSpace(skill.Name)
|
||||
if !isValidSkillName(name) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "invalid skill name")
|
||||
}
|
||||
content := strings.TrimSpace(skill.Content)
|
||||
if content == "" {
|
||||
content = buildSkillContent(name, strings.TrimSpace(skill.Description))
|
||||
}
|
||||
filePath := path.Join(".skills", name, "SKILL.md")
|
||||
if _, err := h.callMCPTool(ctx, containerID, "fs.write", map[string]any{
|
||||
"path": filePath,
|
||||
"content": content,
|
||||
}); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, skillsOpResponse{OK: true})
|
||||
}
|
||||
|
||||
// DeleteSkills godoc
|
||||
// @Summary Delete skills from container
|
||||
// @Tags containerd
|
||||
// @Param payload body SkillsDeleteRequest true "Delete skills payload"
|
||||
// @Success 200 {object} skillsOpResponse
|
||||
// @Failure 400 {object} ErrorResponse
|
||||
// @Failure 404 {object} ErrorResponse
|
||||
// @Failure 500 {object} ErrorResponse
|
||||
// @Router /mcp/skills [delete]
|
||||
func (h *ContainerdHandler) DeleteSkills(c echo.Context) error {
|
||||
userID, err := h.requireUserID(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var req SkillsDeleteRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if len(req.Names) == 0 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "names is required")
|
||||
}
|
||||
|
||||
ctx := c.Request().Context()
|
||||
containerID, err := h.userContainerID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.ensureTaskRunning(ctx, containerID); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
for _, name := range req.Names {
|
||||
skillName := strings.TrimSpace(name)
|
||||
if !isValidSkillName(skillName) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "invalid skill name")
|
||||
}
|
||||
deletePath := path.Join(".skills", skillName)
|
||||
if _, err := h.callMCPTool(ctx, containerID, "fs.delete", map[string]any{
|
||||
"path": deletePath,
|
||||
}); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, skillsOpResponse{OK: true})
|
||||
}
|
||||
|
||||
func (h *ContainerdHandler) ensureSkillsDirHost(userID string) error {
|
||||
dataRoot := strings.TrimSpace(h.cfg.DataRoot)
|
||||
if dataRoot == "" {
|
||||
dataRoot = config.DefaultDataRoot
|
||||
}
|
||||
skillsDir := path.Join(dataRoot, "users", userID, ".skills")
|
||||
return os.MkdirAll(skillsDir, 0o755)
|
||||
}
|
||||
|
||||
func (h *ContainerdHandler) readSkillFile(ctx context.Context, containerID, filePath string) (string, error) {
|
||||
payload, err := h.callMCPTool(ctx, containerID, "fs.read", map[string]any{
|
||||
"path": filePath,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
content, err := extractContentString(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func (h *ContainerdHandler) callMCPTool(ctx context.Context, containerID, toolName string, args map[string]any) (map[string]any, error) {
|
||||
id := "skills-" + strconv.FormatInt(time.Now().UnixNano(), 10)
|
||||
req, err := mcptools.NewToolCallRequest(id, toolName, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload, err := h.callMCPServer(ctx, containerID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := mcptools.PayloadError(payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := mcptools.ResultError(payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func extractListEntries(payload map[string]any) ([]skillEntry, error) {
|
||||
result, err := mcptools.StructuredContent(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawEntries, ok := result["entries"].([]any)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid list response")
|
||||
}
|
||||
entries := make([]skillEntry, 0, len(rawEntries))
|
||||
for _, raw := range rawEntries {
|
||||
entryMap, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
entryPath, _ := entryMap["path"].(string)
|
||||
if entryPath == "" {
|
||||
continue
|
||||
}
|
||||
isDir, _ := entryMap["is_dir"].(bool)
|
||||
entries = append(entries, skillEntry{Path: entryPath, IsDir: isDir})
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
type skillEntry struct {
|
||||
Path string
|
||||
IsDir bool
|
||||
}
|
||||
|
||||
func extractContentString(payload map[string]any) (string, error) {
|
||||
result, err := mcptools.StructuredContent(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
content, _ := result["content"].(string)
|
||||
if content == "" {
|
||||
return "", errors.New("empty content")
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func skillNameFromPath(rel string) string {
|
||||
if rel == "" || rel == "SKILL.md" {
|
||||
return "default"
|
||||
}
|
||||
parent := path.Dir(rel)
|
||||
if parent == "." {
|
||||
return "default"
|
||||
}
|
||||
return path.Base(parent)
|
||||
}
|
||||
|
||||
func skillPathForEntry(entry skillEntry) (string, string) {
|
||||
rel := strings.TrimPrefix(entry.Path, ".skills/")
|
||||
if rel == entry.Path {
|
||||
rel = strings.TrimPrefix(entry.Path, "./.skills/")
|
||||
}
|
||||
if entry.IsDir {
|
||||
name := path.Base(rel)
|
||||
if name == "." || name == "" {
|
||||
return "", ""
|
||||
}
|
||||
return path.Join(".skills", name, "SKILL.md"), name
|
||||
}
|
||||
if path.Base(rel) == "SKILL.md" {
|
||||
return path.Join(".skills", "SKILL.md"), skillNameFromPath(rel)
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func skillDescription(content string) string {
|
||||
lines := strings.Split(content, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "#") {
|
||||
return strings.TrimSpace(strings.TrimPrefix(line, "#"))
|
||||
}
|
||||
return line
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func buildSkillContent(name, description string) string {
|
||||
if description == "" {
|
||||
return "# " + name
|
||||
}
|
||||
return "# " + name + "\n\n" + description
|
||||
}
|
||||
|
||||
func isValidSkillName(name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(name, "..") {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(name, "/") || strings.Contains(name, "\\") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type JSONRPCRequest struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID json.RawMessage `json:"id"`
|
||||
Method string `json:"method"`
|
||||
Params json.RawMessage `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
type JSONRPCResponse struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID json.RawMessage `json:"id,omitempty"`
|
||||
Result any `json:"result,omitempty"`
|
||||
Error *JSONRPCError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type JSONRPCError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func NewToolCallRequest(id string, toolName string, args map[string]any) (JSONRPCRequest, error) {
|
||||
params := map[string]any{
|
||||
"name": toolName,
|
||||
"arguments": args,
|
||||
}
|
||||
rawParams, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return JSONRPCRequest{}, err
|
||||
}
|
||||
return JSONRPCRequest{
|
||||
JSONRPC: "2.0",
|
||||
ID: RawStringID(id),
|
||||
Method: "tools/call",
|
||||
Params: rawParams,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func RawStringID(id string) json.RawMessage {
|
||||
return json.RawMessage([]byte(strconv.Quote(id)))
|
||||
}
|
||||
|
||||
func PayloadError(payload map[string]any) error {
|
||||
if payload == nil {
|
||||
return errors.New("empty payload")
|
||||
}
|
||||
if errObj, ok := payload["error"].(map[string]any); ok {
|
||||
if msg, ok := errObj["message"].(string); ok && msg != "" {
|
||||
return errors.New(msg)
|
||||
}
|
||||
return errors.New("mcp error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ResultError(payload map[string]any) error {
|
||||
result, ok := payload["result"].(map[string]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if isErr, ok := result["isError"].(bool); ok && isErr {
|
||||
msg := ContentText(result)
|
||||
if msg == "" {
|
||||
msg = "mcp tool error"
|
||||
}
|
||||
return errors.New(msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func StructuredContent(payload map[string]any) (map[string]any, error) {
|
||||
result, ok := payload["result"].(map[string]any)
|
||||
if !ok {
|
||||
return nil, errors.New("missing result")
|
||||
}
|
||||
if structured, ok := result["structuredContent"].(map[string]any); ok {
|
||||
return structured, nil
|
||||
}
|
||||
if content := ContentText(result); content != "" {
|
||||
var out map[string]any
|
||||
if err := json.Unmarshal([]byte(content), &out); err == nil {
|
||||
return out, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("missing structured content")
|
||||
}
|
||||
|
||||
func ContentText(result map[string]any) string {
|
||||
rawContent, ok := result["content"].([]any)
|
||||
if !ok || len(rawContent) == 0 {
|
||||
return ""
|
||||
}
|
||||
first, ok := rawContent[0].(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
text, _ := first["text"].(string)
|
||||
return text
|
||||
}
|
||||
@@ -29,9 +29,6 @@ func NewServer(addr string, jwtSecret string, pingHandler *handlers.PingHandler,
|
||||
if path == "/ping" || path == "/api/swagger.json" || path == "/auth/login" {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(path, "/mcp/") {
|
||||
return false
|
||||
}
|
||||
if strings.HasPrefix(path, "/api/docs") {
|
||||
return true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user