mirror of
https://github.com/mickael-kerjean/filestash
synced 2025-12-06 08:22:24 +01:00
feature (mcp): mcp server
This commit is contained in:
parent
f11d27382f
commit
7821bc8681
26 changed files with 1410 additions and 0 deletions
|
|
@ -29,6 +29,7 @@ import (
|
|||
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_backend_webdav"
|
||||
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_editor_wopi"
|
||||
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_console"
|
||||
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_mcp"
|
||||
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_image_ascii"
|
||||
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_image_c"
|
||||
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_image_transcode"
|
||||
|
|
|
|||
33
server/plugin/plg_handler_mcp/config/config.go
Normal file
33
server/plugin/plg_handler_mcp/config/config.go
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package plg_handler_mcp
|
||||
|
||||
import (
|
||||
. "github.com/mickael-kerjean/filestash/server/common"
|
||||
)
|
||||
|
||||
var PluginEnable = func() bool {
|
||||
return Config.Get("features.mcp.enable").Schema(func(f *FormElement) *FormElement {
|
||||
if f == nil {
|
||||
f = &FormElement{}
|
||||
}
|
||||
f.Name = "enable"
|
||||
f.Type = "enable"
|
||||
f.Target = []string{"mcp_can_edit"}
|
||||
f.Description = "Enable/Disable the Model Context Protocol"
|
||||
f.Default = false
|
||||
return f
|
||||
}).Bool()
|
||||
}
|
||||
|
||||
var CanEdit = func() bool {
|
||||
return Config.Get("features.mcp.can_edit").Schema(func(f *FormElement) *FormElement {
|
||||
if f == nil {
|
||||
f = &FormElement{}
|
||||
}
|
||||
f.Id = "mcp_can_edit"
|
||||
f.Name = "can_edit"
|
||||
f.Type = "boolean"
|
||||
f.Description = "Enable/Disable editing"
|
||||
f.Default = false
|
||||
return f
|
||||
}).Bool()
|
||||
}
|
||||
213
server/plugin/plg_handler_mcp/handler.go
Normal file
213
server/plugin/plg_handler_mcp/handler.go
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
package plg_handler_mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
. "github.com/mickael-kerjean/filestash/server/common"
|
||||
"github.com/mickael-kerjean/filestash/server/model"
|
||||
. "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_mcp/impl"
|
||||
. "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_mcp/types"
|
||||
. "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_mcp/utils"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func (this *Server) messageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
sessionID := r.URL.Query().Get("sessionId")
|
||||
if r.Method != http.MethodPost {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Invalid Request"))
|
||||
return
|
||||
}
|
||||
|
||||
request := JSONRPCRequest{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("ERR: " + err.Error()))
|
||||
return
|
||||
}
|
||||
this.GetSession(sessionID).Chan <- request
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (this *Server) sseHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
token := this.ValidateToken(r.Header.Get("Authorization"))
|
||||
if token == "" {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(JSONRPCResponse{
|
||||
JSONRPC: "2.0",
|
||||
Error: &JSONRPCError{
|
||||
Code: http.StatusUnauthorized,
|
||||
Message: "Missing or invalid access token",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
userSession := this.GetSession(uuid.New().String())
|
||||
userSession.Token = token
|
||||
if b, err := getBackend(userSession.Token); err == nil {
|
||||
userSession.HomeDir, _ = model.GetHome(b, "/")
|
||||
userSession.CurrDir = ToString(userSession.HomeDir, "/")
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
|
||||
fmt.Fprintf(w, "event: endpoint\ndata: %s?sessionId=%s\n\n", "/messages", userSession.Id)
|
||||
w.(http.Flusher).Flush()
|
||||
|
||||
for {
|
||||
select {
|
||||
case request := <-userSession.Chan:
|
||||
b, err := getBackend(userSession.Token)
|
||||
if err != nil {
|
||||
if err == ErrNotAuthorized {
|
||||
err = JSONRPCError{
|
||||
Code: ErrNotAuthorized.Status(),
|
||||
Message: "You aren't authenticated",
|
||||
}
|
||||
}
|
||||
SendError(w, request.ID, err)
|
||||
break
|
||||
}
|
||||
userSession.Backend = b
|
||||
|
||||
switch request.Method {
|
||||
case "initialize":
|
||||
SendMessage(w, request.ID, InitializeResult{
|
||||
ProtocolVersion: "2024-11-05",
|
||||
ServerInfo: ServerInfo{
|
||||
Name: "Universal Storage Server",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
Capabilities: Capabilities{
|
||||
Tools: map[string]interface{}{
|
||||
"listChanged": true,
|
||||
},
|
||||
Resources: map[string]interface{}{},
|
||||
Prompts: map[string]interface{}{},
|
||||
},
|
||||
})
|
||||
case "resources/list":
|
||||
SendMessage(w, request.ID, &CallResourcesList{
|
||||
Resources: AllResources(),
|
||||
})
|
||||
case "resources/templates/list":
|
||||
SendMessage(w, request.ID, &CallResourceTemplatesList{
|
||||
ResourceTemplates: AllResourceTemplates(),
|
||||
})
|
||||
case "resources/read":
|
||||
SendMessage(w, request.ID, &CallResourceRead{
|
||||
Contents: ExecResourceRead(request.Params),
|
||||
})
|
||||
case "prompts/list":
|
||||
SendMessage(w, request.ID, &CallPromptsList{
|
||||
Prompts: AllPrompts(),
|
||||
})
|
||||
case "prompts/get":
|
||||
if m, ok := request.Params["name"].(string); ok {
|
||||
res, err := ExecPromptGet(m, request.Params, &userSession)
|
||||
if err == nil {
|
||||
SendMessage(w, request.ID, CallPromptGet{
|
||||
Messages: res,
|
||||
Description: ExecPromptDescription(request.Params),
|
||||
})
|
||||
} else {
|
||||
SendError(w, request.ID, err)
|
||||
}
|
||||
} else {
|
||||
SendError(w, request.ID, JSONRPCError{
|
||||
Code: http.StatusBadRequest,
|
||||
Message: fmt.Sprintf("Unknown prompt name: %v", request.Params["name"]),
|
||||
})
|
||||
}
|
||||
case "tools/list":
|
||||
SendMessage(w, request.ID, &CallListTools{
|
||||
Tools: AllTools(),
|
||||
})
|
||||
case "tools/call":
|
||||
if m, ok := request.Params["name"].(string); ok {
|
||||
res, err := ExecTool(m, request.Params, &userSession)
|
||||
if err == nil {
|
||||
SendMessage(w, request.ID, CallTool{
|
||||
Content: []TextContent{*res},
|
||||
})
|
||||
} else {
|
||||
SendError(w, request.ID, err)
|
||||
}
|
||||
} else {
|
||||
SendError(w, request.ID, JSONRPCError{
|
||||
Code: http.StatusBadRequest,
|
||||
Message: fmt.Sprintf("Unknown tool name: %v", request.Params["name"]),
|
||||
})
|
||||
}
|
||||
case "notifications/initialized":
|
||||
SendMessage(w, request.ID, map[string]string{})
|
||||
case "completion/complete":
|
||||
SendMessage(w, request.ID, CallCompletionResult{
|
||||
Completion: ExecCompletion(request.Params, &userSession),
|
||||
})
|
||||
case "ping":
|
||||
SendMessage(w, request.ID, map[string]string{})
|
||||
default:
|
||||
if request.Method == "" && userSession.Ping.ID == request.ID { // response to ping
|
||||
userSession.Ping.LastResponse = time.Now()
|
||||
userSession.Ping.ID += 1
|
||||
} else {
|
||||
Log.Warning("plg_handler_mcp::sse message=unknown_method method=%s requestID=%d", request.Method, request.ID)
|
||||
SendError(w, request.ID, JSONRPCError{
|
||||
Code: http.StatusMethodNotAllowed,
|
||||
Message: fmt.Sprintf("Unknown request: %s", request.Method),
|
||||
})
|
||||
}
|
||||
}
|
||||
case <-r.Context().Done():
|
||||
this.RemoveSession(&userSession)
|
||||
return
|
||||
case <-time.After(15 * time.Second):
|
||||
SendPing(w, userSession.Ping.ID)
|
||||
if time.Since(userSession.Ping.LastResponse) > 60*time.Second {
|
||||
SendMethod(w, userSession.Ping.ID+1, "notifications/cancelled", map[string]interface{}{
|
||||
"requestId": userSession.Ping.ID,
|
||||
"reason": "Request timed out",
|
||||
})
|
||||
time.Sleep(2 * time.Second)
|
||||
this.RemoveSession(&userSession)
|
||||
if hi, ok := w.(http.Hijacker); ok {
|
||||
if conn, rw, err := hi.Hijack(); err == nil {
|
||||
rw.WriteString("0\r\n\r\n")
|
||||
rw.Flush()
|
||||
time.Sleep(1 * time.Second)
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getBackend(token string) (IBackend, error) {
|
||||
str, err := DecryptString(SECRET_KEY_DERIVATE_FOR_USER, token)
|
||||
if err != nil {
|
||||
return nil, ErrNotAuthorized
|
||||
}
|
||||
session := map[string]string{}
|
||||
if err = json.Unmarshal([]byte(str), &session); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return model.NewBackend(&App{
|
||||
Context: context.Background(),
|
||||
}, session)
|
||||
}
|
||||
116
server/plugin/plg_handler_mcp/handler_auth.go
Normal file
116
server/plugin/plg_handler_mcp/handler_auth.go
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
package plg_handler_mcp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
. "github.com/mickael-kerjean/filestash/server/common"
|
||||
. "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_mcp/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
DEFAULT_TOKEN_EXPIRY = 3600
|
||||
)
|
||||
|
||||
func (this Server) WellKnownInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
WithCors(w)
|
||||
if r.Method != http.MethodGet && r.Method != http.MethodOptions {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
host := r.Host
|
||||
baseURL := fmt.Sprintf("%s://%s", scheme, host)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"issuer": baseURL,
|
||||
"authorization_endpoint": fmt.Sprintf("%s/mcp/authorize", baseURL),
|
||||
"token_endpoint": fmt.Sprintf("%s/mcp/token", baseURL),
|
||||
"registration_endpoint": fmt.Sprintf("%s/mcp/register", baseURL),
|
||||
"response_types_supported": []string{"code"},
|
||||
"grant_types_supported": []string{"authorization_code"},
|
||||
"token_endpoint_auth_methods_supported": []string{
|
||||
"none",
|
||||
},
|
||||
"code_challenge_methods_supported": []string{
|
||||
"S256",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (this Server) AuthorizeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
WithCors(w)
|
||||
|
||||
responseType := r.URL.Query().Get("response_type")
|
||||
clientID := r.URL.Query().Get("client_id")
|
||||
redirectURI := r.URL.Query().Get("redirect_uri")
|
||||
|
||||
if responseType != "code" {
|
||||
http.Error(w, "response_type must be 'code'", http.StatusBadRequest)
|
||||
return
|
||||
} else if clientID == "" {
|
||||
http.Error(w, "client_id is required", http.StatusBadRequest)
|
||||
return
|
||||
} else if redirectURI == "" {
|
||||
http.Error(w, "redirect_uri is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, fmt.Sprintf("/login?next=/api/mcp?redirect_uri=%s", redirectURI), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (this Server) TokenHandler(w http.ResponseWriter, r *http.Request) {
|
||||
WithCors(w)
|
||||
if r.Method != http.MethodPost && r.Method != http.MethodOptions {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if grantType := r.FormValue("grant_type"); grantType != "authorization_code" {
|
||||
http.Error(w, "Invalid Grant Type", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"access_token": r.FormValue("code"),
|
||||
"token_type": "Bearer",
|
||||
})
|
||||
}
|
||||
|
||||
func (this Server) RegisterHandler(w http.ResponseWriter, r *http.Request) {
|
||||
WithCors(w)
|
||||
if r.Method != http.MethodPost && r.Method != http.MethodOptions {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"client_id": "anonymous",
|
||||
"client_secret": "anonymous",
|
||||
"client_id_issued_at": time.Now().Unix(),
|
||||
"client_secret_expires_at": 0,
|
||||
"client_name": "Untrusted",
|
||||
"redirect_uris": []string{},
|
||||
"grant_types": []string{"authorization_code"},
|
||||
"token_endpoint_auth_method": "client_secret_basic",
|
||||
})
|
||||
}
|
||||
|
||||
func (this Server) CallbackHandler(ctx *App, res http.ResponseWriter, req *http.Request) {
|
||||
uri := req.URL.Query().Get("redirect_uri")
|
||||
if uri == "" {
|
||||
SendErrorResult(res, ErrNotValid)
|
||||
return
|
||||
}
|
||||
http.Redirect(res, req, fmt.Sprintf(uri+"?code=%s", ctx.Authorization), http.StatusSeeOther)
|
||||
}
|
||||
39
server/plugin/plg_handler_mcp/handler_state.go
Normal file
39
server/plugin/plg_handler_mcp/handler_state.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package plg_handler_mcp
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
. "github.com/mickael-kerjean/filestash/server/common"
|
||||
. "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_mcp/types"
|
||||
)
|
||||
|
||||
func (this *Server) RemoveSession(userSession *UserSession) {
|
||||
this.expired.Store(Hash(userSession.Token, 16), nil)
|
||||
this.sessions.Delete(userSession.Id)
|
||||
}
|
||||
|
||||
func (this *Server) ValidateToken(token string) string {
|
||||
if hasToken := strings.HasPrefix(token, "Bearer "); hasToken == false {
|
||||
return ""
|
||||
}
|
||||
token = strings.TrimPrefix(token, "Bearer ")
|
||||
if _, ok := this.expired.Load(Hash(token, 16)); ok {
|
||||
return ""
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
func (this *Server) GetSession(uuid string) UserSession {
|
||||
ch, _ := this.sessions.LoadOrStore(uuid, UserSession{
|
||||
Id: uuid,
|
||||
Chan: make(chan JSONRPCRequest),
|
||||
CurrDir: "/",
|
||||
HomeDir: "/",
|
||||
Ping: Ping{
|
||||
ID: 0,
|
||||
LastResponse: time.Now(),
|
||||
},
|
||||
})
|
||||
return ch.(UserSession)
|
||||
}
|
||||
50
server/plugin/plg_handler_mcp/impl/completion.go
Normal file
50
server/plugin/plg_handler_mcp/impl/completion.go
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
package impl
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
. "github.com/mickael-kerjean/filestash/server/common"
|
||||
. "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_mcp/types"
|
||||
. "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_mcp/utils"
|
||||
)
|
||||
|
||||
func ExecCompletion(params map[string]any, userSession *UserSession) Completion {
|
||||
if path := GetArgumentString(params, "value"); path != "" && GetArgumentString(params, "name") == "path" {
|
||||
fpath := filepath.Dir(path)
|
||||
fname := filepath.Base(path)
|
||||
if strings.HasSuffix(path, "/") {
|
||||
fname = ""
|
||||
}
|
||||
files, err := userSession.Backend.Ls(EnforceDirectory(fpath))
|
||||
if err == nil {
|
||||
values := []string{}
|
||||
for _, file := range files {
|
||||
val := JoinPath(fpath, file.Name())
|
||||
if file.IsDir() {
|
||||
val = EnforceDirectory(val)
|
||||
}
|
||||
|
||||
if fname == "" && strings.HasPrefix(file.Name(), ".") == false {
|
||||
values = append(values, val)
|
||||
} else if fname != "" && strings.HasPrefix(file.Name(), fname) {
|
||||
values = append(values, val)
|
||||
}
|
||||
|
||||
if len(values) >= 100 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return Completion{
|
||||
Values: values,
|
||||
Total: uint64(len(values)),
|
||||
HasMore: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
return Completion{
|
||||
Values: []string{},
|
||||
Total: 0,
|
||||
HasMore: false,
|
||||
}
|
||||
}
|
||||
46
server/plugin/plg_handler_mcp/impl/prompts.go
Normal file
46
server/plugin/plg_handler_mcp/impl/prompts.go
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
package impl
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
. "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_mcp/types"
|
||||
)
|
||||
|
||||
var listOfPrompts = map[string]PromptDefinition{}
|
||||
|
||||
type PromptDefinition struct {
|
||||
Prompt
|
||||
ExecMessage func(params map[string]any, userSession *UserSession) ([]PromptMessage, error)
|
||||
ExecDescription func(params map[string]any) string
|
||||
}
|
||||
|
||||
func RegisterPrompt(t PromptDefinition) {
|
||||
listOfPrompts[t.Name] = t
|
||||
}
|
||||
|
||||
func AllPrompts() []Prompt {
|
||||
t := []Prompt{}
|
||||
for _, v := range listOfPrompts {
|
||||
t = append(t, v.Prompt)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func ExecPromptGet(name string, params map[string]any, userSession *UserSession) ([]PromptMessage, error) {
|
||||
t, ok := listOfPrompts[name]
|
||||
if !ok {
|
||||
return nil, &JSONRPCError{
|
||||
Code: http.StatusNotFound,
|
||||
Message: "Not Found",
|
||||
}
|
||||
}
|
||||
return t.ExecMessage(params, userSession)
|
||||
}
|
||||
|
||||
func ExecPromptDescription(params map[string]any) string {
|
||||
n, ok := params["name"].(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return n
|
||||
}
|
||||
113
server/plugin/plg_handler_mcp/impl/prompts_fs.go
Normal file
113
server/plugin/plg_handler_mcp/impl/prompts_fs.go
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
package impl
|
||||
|
||||
import (
|
||||
. "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_mcp/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterPrompt(PromptDefinition{
|
||||
Prompt: Prompt{
|
||||
Name: "ls",
|
||||
Description: "list directory contents",
|
||||
Arguments: []PromptArgument{
|
||||
{
|
||||
Name: "path",
|
||||
Description: "path where the query is made",
|
||||
Required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
ExecMessage: func(params map[string]any, userSession *UserSession) ([]PromptMessage, error) {
|
||||
return []PromptMessage{
|
||||
{
|
||||
Role: "user",
|
||||
Content: TextContent{
|
||||
Type: "text",
|
||||
Text: "call ls(path)",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
ExecDescription: func(params map[string]any) string {
|
||||
return "list directory contents"
|
||||
},
|
||||
})
|
||||
|
||||
RegisterPrompt(PromptDefinition{
|
||||
Prompt: Prompt{
|
||||
Name: "cat",
|
||||
Description: "read a file at a specified path",
|
||||
Arguments: []PromptArgument{
|
||||
{
|
||||
Name: "path",
|
||||
Description: "path where the query is made",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
ExecMessage: func(params map[string]any, userSession *UserSession) ([]PromptMessage, error) {
|
||||
return []PromptMessage{
|
||||
{
|
||||
Role: "user",
|
||||
Content: TextContent{
|
||||
Type: "text",
|
||||
Text: "call cat(path)",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
ExecDescription: func(params map[string]any) string {
|
||||
return "read a file at a specified path"
|
||||
},
|
||||
})
|
||||
|
||||
RegisterPrompt(PromptDefinition{
|
||||
Prompt: Prompt{
|
||||
Name: "pwd",
|
||||
Description: "print name of current/working directory",
|
||||
Arguments: []PromptArgument{},
|
||||
},
|
||||
ExecMessage: func(params map[string]any, userSession *UserSession) ([]PromptMessage, error) {
|
||||
return []PromptMessage{
|
||||
{
|
||||
Role: "user",
|
||||
Content: TextContent{
|
||||
Type: "text",
|
||||
Text: "call pwd",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
ExecDescription: func(params map[string]any) string {
|
||||
return "print name of current/working directory"
|
||||
},
|
||||
})
|
||||
|
||||
RegisterPrompt(PromptDefinition{
|
||||
Prompt: Prompt{
|
||||
Name: "cd",
|
||||
Description: "change the working directory",
|
||||
Arguments: []PromptArgument{
|
||||
{
|
||||
Name: "path",
|
||||
Description: "path where the query is made",
|
||||
Required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
ExecMessage: func(params map[string]any, userSession *UserSession) ([]PromptMessage, error) {
|
||||
return []PromptMessage{
|
||||
{
|
||||
Role: "user",
|
||||
Content: TextContent{
|
||||
Type: "text",
|
||||
Text: "call cd(path)",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
ExecDescription: func(params map[string]any) string {
|
||||
return "change the working directory"
|
||||
},
|
||||
})
|
||||
}
|
||||
17
server/plugin/plg_handler_mcp/impl/resources.go
Normal file
17
server/plugin/plg_handler_mcp/impl/resources.go
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
package impl
|
||||
|
||||
import (
|
||||
. "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_mcp/types"
|
||||
)
|
||||
|
||||
func AllResources() []Resource {
|
||||
return []Resource{}
|
||||
}
|
||||
|
||||
func AllResourceTemplates() []ResourceTemplate {
|
||||
return []ResourceTemplate{}
|
||||
}
|
||||
|
||||
func ExecResourceRead(params map[string]any) []ResourceContent {
|
||||
return []ResourceContent{}
|
||||
}
|
||||
37
server/plugin/plg_handler_mcp/impl/tools.go
Normal file
37
server/plugin/plg_handler_mcp/impl/tools.go
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
package impl
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
. "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_mcp/types"
|
||||
)
|
||||
|
||||
var listOfTools = map[string]ToolDefinition{}
|
||||
|
||||
type ToolDefinition struct {
|
||||
Tool
|
||||
Exec func(params map[string]any, userSession *UserSession) (*TextContent, error)
|
||||
}
|
||||
|
||||
func RegisterTool(t ToolDefinition) {
|
||||
listOfTools[t.Name] = t
|
||||
}
|
||||
|
||||
func AllTools() []Tool {
|
||||
t := []Tool{}
|
||||
for _, v := range listOfTools {
|
||||
t = append(t, v.Tool)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func ExecTool(name string, params map[string]any, userSession *UserSession) (*TextContent, error) {
|
||||
td, ok := listOfTools[name]
|
||||
if !ok {
|
||||
return nil, JSONRPCError{
|
||||
Code: http.StatusNotImplemented,
|
||||
Message: "Not Found",
|
||||
}
|
||||
}
|
||||
return td.Exec(params, userSession)
|
||||
}
|
||||
340
server/plugin/plg_handler_mcp/impl/tools_fs.go
Normal file
340
server/plugin/plg_handler_mcp/impl/tools_fs.go
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
package impl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
. "github.com/mickael-kerjean/filestash/server/common"
|
||||
. "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_mcp/config"
|
||||
. "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_mcp/types"
|
||||
. "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_mcp/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Hooks.Register.Onload(func() {
|
||||
RegisterTool(ToolDefinition{
|
||||
Tool: Tool{
|
||||
Name: "ls",
|
||||
Description: "list directory contents",
|
||||
InputSchema: JsonSchema(map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"path": map[string]string{
|
||||
"type": "string",
|
||||
"description": "path where the query is made",
|
||||
},
|
||||
},
|
||||
"required": []string{},
|
||||
}),
|
||||
},
|
||||
Exec: ToolFSLs,
|
||||
})
|
||||
|
||||
RegisterTool(ToolDefinition{
|
||||
Tool: Tool{
|
||||
Name: "cat",
|
||||
Description: "read a file at a specified path.",
|
||||
InputSchema: JsonSchema(map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"path": map[string]string{
|
||||
"type": "string",
|
||||
"description": "path where the query is made",
|
||||
},
|
||||
},
|
||||
"required": []string{"path"},
|
||||
}),
|
||||
},
|
||||
Exec: ToolFSCat,
|
||||
})
|
||||
|
||||
RegisterTool(ToolDefinition{
|
||||
Tool: Tool{
|
||||
Name: "pwd",
|
||||
Description: "print name of current/working directory",
|
||||
InputSchema: JsonSchema(map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{},
|
||||
"required": []string{},
|
||||
}),
|
||||
},
|
||||
Exec: ToolFSPwd,
|
||||
})
|
||||
|
||||
RegisterTool(ToolDefinition{
|
||||
Tool: Tool{
|
||||
Name: "cd",
|
||||
Description: "change the working directory",
|
||||
InputSchema: JsonSchema(map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"path": map[string]string{
|
||||
"type": "string",
|
||||
"description": "path where the query is made",
|
||||
},
|
||||
},
|
||||
"required": []string{"path"},
|
||||
}),
|
||||
},
|
||||
Exec: ToolFSCd,
|
||||
})
|
||||
|
||||
if !CanEdit() {
|
||||
return
|
||||
}
|
||||
|
||||
RegisterTool(ToolDefinition{
|
||||
Tool: Tool{
|
||||
Name: "mv",
|
||||
Description: "move (rename) files",
|
||||
InputSchema: JsonSchema(map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"from": map[string]string{
|
||||
"type": "string",
|
||||
"description": "origin path",
|
||||
},
|
||||
"to": map[string]string{
|
||||
"type": "string",
|
||||
"description": "destination path",
|
||||
},
|
||||
},
|
||||
"required": []string{"from", "to"},
|
||||
}),
|
||||
},
|
||||
Exec: ToolFSMv,
|
||||
})
|
||||
|
||||
RegisterTool(ToolDefinition{
|
||||
Tool: Tool{
|
||||
Name: "mkdir",
|
||||
Description: "make directories",
|
||||
InputSchema: JsonSchema(map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"path": map[string]string{
|
||||
"type": "string",
|
||||
"description": "path where the query is made",
|
||||
},
|
||||
},
|
||||
"required": []string{"path"},
|
||||
}),
|
||||
},
|
||||
Exec: ToolFSMkdir,
|
||||
})
|
||||
|
||||
RegisterTool(ToolDefinition{
|
||||
Tool: Tool{
|
||||
Name: "touch",
|
||||
Description: "create file",
|
||||
InputSchema: JsonSchema(map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"path": map[string]string{
|
||||
"type": "string",
|
||||
"description": "path where the query is made",
|
||||
},
|
||||
},
|
||||
"required": []string{"path"},
|
||||
}),
|
||||
},
|
||||
Exec: ToolFSTouch,
|
||||
})
|
||||
|
||||
RegisterTool(ToolDefinition{
|
||||
Tool: Tool{
|
||||
Name: "rm",
|
||||
Description: "remove files or directories",
|
||||
InputSchema: JsonSchema(map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"path": map[string]string{
|
||||
"type": "string",
|
||||
"description": "path where the query is made",
|
||||
},
|
||||
},
|
||||
"required": []string{"path"},
|
||||
}),
|
||||
},
|
||||
Exec: ToolFSRm,
|
||||
})
|
||||
|
||||
RegisterTool(ToolDefinition{
|
||||
Tool: Tool{
|
||||
Name: "save",
|
||||
Description: "save a file",
|
||||
InputSchema: JsonSchema(map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"path": map[string]string{
|
||||
"type": "string",
|
||||
"description": "path where the query is made",
|
||||
},
|
||||
"content": map[string]string{
|
||||
"type": "string",
|
||||
"description": "content of the file",
|
||||
},
|
||||
},
|
||||
"required": []string{"path"},
|
||||
}),
|
||||
},
|
||||
Exec: ToolFSSave,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func ToolFSLs(params map[string]any, userSession *UserSession) (*TextContent, error) {
|
||||
files, err := userSession.Backend.Ls(EnforceDirectory(getPath(params, userSession, "path")))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var b bytes.Buffer
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
b.Write([]byte("[DIR] "))
|
||||
} else {
|
||||
b.Write([]byte("[FILE] "))
|
||||
}
|
||||
b.Write([]byte(file.Name()))
|
||||
b.Write([]byte("\n"))
|
||||
}
|
||||
return &TextContent{
|
||||
Type: "text",
|
||||
Text: b.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ToolFSCat(params map[string]any, userSession *UserSession) (*TextContent, error) {
|
||||
if isArgEmpty(params, "path") {
|
||||
return nil, ErrNotValid
|
||||
}
|
||||
r, err := userSession.Backend.Cat(getPath(params, userSession, "path"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b, err := io.ReadAll(r)
|
||||
r.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &TextContent{
|
||||
Type: "text",
|
||||
Text: string(b),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ToolFSPwd(params map[string]any, userSession *UserSession) (*TextContent, error) {
|
||||
return &TextContent{
|
||||
Type: "text",
|
||||
Text: userSession.CurrDir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ToolFSCd(params map[string]any, userSession *UserSession) (*TextContent, error) {
|
||||
path := EnforceDirectory(getPath(params, userSession, "path"))
|
||||
if _, err := userSession.Backend.Ls(path); err != nil {
|
||||
return nil, errors.New("No such file or directory")
|
||||
}
|
||||
userSession.CurrDir = EnforceDirectory(path)
|
||||
return &TextContent{
|
||||
Type: "text",
|
||||
Text: userSession.CurrDir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ToolFSMv(params map[string]any, userSession *UserSession) (*TextContent, error) {
|
||||
if isArgEmpty(params, "from") || isArgEmpty(params, "to") {
|
||||
return nil, ErrNotValid
|
||||
}
|
||||
if err := userSession.Backend.Mv(
|
||||
getPath(params, userSession, "from"),
|
||||
getPath(params, userSession, "to"),
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &TextContent{
|
||||
Type: "text",
|
||||
Text: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ToolFSMkdir(params map[string]any, userSession *UserSession) (*TextContent, error) {
|
||||
if isArgEmpty(params, "path") {
|
||||
return nil, ErrNotValid
|
||||
}
|
||||
if err := userSession.Backend.Mkdir(EnforceDirectory(getPath(params, userSession, "path"))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &TextContent{
|
||||
Type: "text",
|
||||
Text: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ToolFSTouch(params map[string]any, userSession *UserSession) (*TextContent, error) {
|
||||
if isArgEmpty(params, "path") {
|
||||
return nil, ErrNotValid
|
||||
}
|
||||
if err := userSession.Backend.Touch(getPath(params, userSession, "path")); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &TextContent{
|
||||
Type: "text",
|
||||
Text: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ToolFSRm(params map[string]any, userSession *UserSession) (*TextContent, error) {
|
||||
if isArgEmpty(params, "path") {
|
||||
return nil, ErrNotValid
|
||||
}
|
||||
if err := userSession.Backend.Rm(getPath(params, userSession, "path")); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &TextContent{
|
||||
Type: "text",
|
||||
Text: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ToolFSSave(params map[string]any, userSession *UserSession) (*TextContent, error) {
|
||||
if isArgEmpty(params, "path") {
|
||||
return nil, ErrNotValid
|
||||
}
|
||||
if err := userSession.Backend.Save(
|
||||
getPath(params, userSession, "path"),
|
||||
NewReadCloserFromBytes([]byte(GetArgumentsString(params, "content"))),
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &TextContent{
|
||||
Type: "text",
|
||||
Text: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getPath(params map[string]any, userSession *UserSession, name string) string {
|
||||
path := GetArgumentsString(params, name)
|
||||
currDir := ""
|
||||
if path == "" {
|
||||
currDir = userSession.CurrDir
|
||||
} else if strings.HasPrefix(path, "~/") {
|
||||
currDir = "." + strings.TrimPrefix(path, "~")
|
||||
currDir = JoinPath(userSession.HomeDir, currDir)
|
||||
} else if strings.HasPrefix(path, "/") {
|
||||
currDir = path
|
||||
} else {
|
||||
currDir = filepath.Join(userSession.CurrDir, ToString(path, "./"))
|
||||
}
|
||||
return currDir
|
||||
}
|
||||
|
||||
func isArgEmpty(params map[string]any, name string) bool {
|
||||
if arg := GetArgumentsString(params, name); arg == "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
42
server/plugin/plg_handler_mcp/index.go
Normal file
42
server/plugin/plg_handler_mcp/index.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package plg_handler_mcp
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
. "github.com/mickael-kerjean/filestash/server/common"
|
||||
. "github.com/mickael-kerjean/filestash/server/middleware"
|
||||
. "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_mcp/config"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
sessions sync.Map
|
||||
expired sync.Map
|
||||
}
|
||||
|
||||
func init() {
|
||||
Hooks.Register.Onload(func() {
|
||||
PluginEnable()
|
||||
CanEdit()
|
||||
})
|
||||
|
||||
Hooks.Register.HttpEndpoint(func(r *mux.Router, app *App) error {
|
||||
if !PluginEnable() {
|
||||
return nil
|
||||
}
|
||||
srv := Server{}
|
||||
r.HandleFunc("/sse", srv.sseHandler)
|
||||
r.HandleFunc("/messages", srv.messageHandler)
|
||||
r.HandleFunc("/.well-known/oauth-authorization-server", srv.WellKnownInfoHandler)
|
||||
r.HandleFunc("/mcp/authorize", srv.AuthorizeHandler)
|
||||
r.HandleFunc("/mcp/token", srv.TokenHandler)
|
||||
r.HandleFunc("/mcp/register", srv.RegisterHandler)
|
||||
r.HandleFunc("/api/mcp", NewMiddlewareChain(
|
||||
srv.CallbackHandler,
|
||||
[]Middleware{SessionStart, LoggedInOnly},
|
||||
*app,
|
||||
))
|
||||
return nil
|
||||
})
|
||||
}
|
||||
11
server/plugin/plg_handler_mcp/types/mcp_completion.go
Normal file
11
server/plugin/plg_handler_mcp/types/mcp_completion.go
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
package types
|
||||
|
||||
type CallCompletionResult struct {
|
||||
Completion Completion `json:"completion"`
|
||||
}
|
||||
|
||||
type Completion struct {
|
||||
Values []string `json:"values"`
|
||||
Total uint64 `json:"total"`
|
||||
HasMore bool `json:"hasMore"`
|
||||
}
|
||||
18
server/plugin/plg_handler_mcp/types/mcp_init.go
Normal file
18
server/plugin/plg_handler_mcp/types/mcp_init.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package types
|
||||
|
||||
type InitializeResult struct {
|
||||
ProtocolVersion string `json:"protocolVersion"`
|
||||
ServerInfo ServerInfo `json:"serverInfo"`
|
||||
Capabilities Capabilities `json:"capabilities"`
|
||||
}
|
||||
|
||||
type ServerInfo struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type Capabilities struct {
|
||||
Tools map[string]interface{} `json:"tools",omitempty`
|
||||
Resources map[string]interface{} `json:"resources",omitempty`
|
||||
Prompts map[string]interface{} `json:"prompts",omitempty`
|
||||
}
|
||||
18
server/plugin/plg_handler_mcp/types/mcp_notification.go
Normal file
18
server/plugin/plg_handler_mcp/types/mcp_notification.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package types
|
||||
|
||||
var (
|
||||
ErrToolsListChanges = newError("notifications/tools/list_changed")
|
||||
ErrDisconnect = newError("internal/disconnect")
|
||||
)
|
||||
|
||||
func newError(s string) error {
|
||||
return notification{s}
|
||||
}
|
||||
|
||||
type notification struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func (this notification) Error() string {
|
||||
return this.msg
|
||||
}
|
||||
28
server/plugin/plg_handler_mcp/types/mcp_prompts.go
Normal file
28
server/plugin/plg_handler_mcp/types/mcp_prompts.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package types
|
||||
|
||||
type CallPromptsList struct {
|
||||
Prompts []Prompt `json:"prompts"`
|
||||
NextCursor string `json:"nextCursor",omitempty`
|
||||
}
|
||||
|
||||
type Prompt struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Arguments []PromptArgument `json:"arguments"`
|
||||
}
|
||||
|
||||
type PromptArgument struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Required bool `json:"required"`
|
||||
}
|
||||
|
||||
type CallPromptGet struct {
|
||||
Description string `json:"description"`
|
||||
Messages []PromptMessage `json:"messages"`
|
||||
}
|
||||
|
||||
type PromptMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content TextContent `json:"content"`
|
||||
}
|
||||
33
server/plugin/plg_handler_mcp/types/mcp_resources.go
Normal file
33
server/plugin/plg_handler_mcp/types/mcp_resources.go
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package types
|
||||
|
||||
type CallResourcesList struct {
|
||||
Resources []Resource `json:"resources"`
|
||||
}
|
||||
|
||||
type CallResourceTemplatesList struct {
|
||||
ResourceTemplates []ResourceTemplate `json:"resourceTemplates"`
|
||||
}
|
||||
|
||||
type CallResourceRead struct {
|
||||
Contents []ResourceContent `json:"contents"`
|
||||
}
|
||||
|
||||
type Resource struct {
|
||||
URI string `json:"uri"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
MimeType string `json:"mimeType"`
|
||||
}
|
||||
|
||||
type ResourceTemplate struct {
|
||||
URITemplate string `json:"uriTemplate"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
MimeType string `json:"mimeType"`
|
||||
}
|
||||
|
||||
type ResourceContent struct {
|
||||
URI string `json:"uri"`
|
||||
MimeType string `json:"mimeType"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
19
server/plugin/plg_handler_mcp/types/mcp_tools.go
Normal file
19
server/plugin/plg_handler_mcp/types/mcp_tools.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type CallListTools struct {
|
||||
Tools []Tool `json:"tools"`
|
||||
}
|
||||
|
||||
type Tool struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
InputSchema json.RawMessage `json:"inputSchema"`
|
||||
}
|
||||
|
||||
type CallTool struct {
|
||||
Content []TextContent `json:"content"`
|
||||
}
|
||||
26
server/plugin/plg_handler_mcp/types/resources.go
Normal file
26
server/plugin/plg_handler_mcp/types/resources.go
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
package types
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type IResource interface {
|
||||
Resource() ([]json.RawMessage, error)
|
||||
}
|
||||
|
||||
type TextContent struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type BinaryContent struct {
|
||||
URI string `json:uri"`
|
||||
MimeType string `json:"mimeType"`
|
||||
Blob []byte `json:"blob"`
|
||||
}
|
||||
|
||||
func (this TextContent) Resource() ([]byte, error) {
|
||||
return json.Marshal(this)
|
||||
}
|
||||
|
||||
func (this BinaryContent) Resource() ([]byte, error) {
|
||||
return json.Marshal(this)
|
||||
}
|
||||
31
server/plugin/plg_handler_mcp/types/rpc.go
Normal file
31
server/plugin/plg_handler_mcp/types/rpc.go
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
package types
|
||||
|
||||
type JSONRPCRequest struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID uint64 `json:"id"`
|
||||
Method string `json:"method"`
|
||||
Params map[string]any `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
type JSONRPCResponse struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID uint64 `json:"id"`
|
||||
Result *any `json:"result,omitempty"`
|
||||
Error *JSONRPCError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type JSONRPCMethod struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
Method string `json:"method"`
|
||||
Params interface{} `json:"params",omitempty`
|
||||
}
|
||||
|
||||
type JSONRPCError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func (this JSONRPCError) Error() string {
|
||||
return this.Message
|
||||
}
|
||||
22
server/plugin/plg_handler_mcp/types/session.go
Normal file
22
server/plugin/plg_handler_mcp/types/session.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
. "github.com/mickael-kerjean/filestash/server/common"
|
||||
)
|
||||
|
||||
type UserSession struct {
|
||||
Id string
|
||||
Chan chan JSONRPCRequest
|
||||
HomeDir string
|
||||
CurrDir string
|
||||
Token string
|
||||
Backend IBackend
|
||||
Ping Ping
|
||||
}
|
||||
|
||||
type Ping struct {
|
||||
ID uint64
|
||||
LastResponse time.Time
|
||||
}
|
||||
10
server/plugin/plg_handler_mcp/utils/cors.go
Normal file
10
server/plugin/plg_handler_mcp/utils/cors.go
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func WithCors(w http.ResponseWriter) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "mcp-protocol-version, Content-Type")
|
||||
}
|
||||
14
server/plugin/plg_handler_mcp/utils/default.go
Normal file
14
server/plugin/plg_handler_mcp/utils/default.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func ToString(val any, def string) string {
|
||||
if val == nil {
|
||||
return def
|
||||
} else if val == "" {
|
||||
return def
|
||||
}
|
||||
return fmt.Sprintf("%v", val)
|
||||
}
|
||||
15
server/plugin/plg_handler_mcp/utils/json.go
Normal file
15
server/plugin/plg_handler_mcp/utils/json.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
func JsonSchema(in any) json.RawMessage {
|
||||
b, _ := json.Marshal(in)
|
||||
return json.RawMessage(b)
|
||||
}
|
||||
|
||||
func JsonText(in any) string {
|
||||
b, _ := json.MarshalIndent(in, "", " ")
|
||||
return string(b)
|
||||
}
|
||||
25
server/plugin/plg_handler_mcp/utils/mcp.go
Normal file
25
server/plugin/plg_handler_mcp/utils/mcp.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package utils
|
||||
|
||||
func GetArgumentsString(params map[string]any, name string) string {
|
||||
m, ok := params["arguments"].(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
p, ok := m[name].(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func GetArgumentString(params map[string]any, name string) string {
|
||||
m, ok := params["argument"].(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
p, ok := m[name].(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return p
|
||||
}
|
||||
93
server/plugin/plg_handler_mcp/utils/response.go
Normal file
93
server/plugin/plg_handler_mcp/utils/response.go
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
. "github.com/mickael-kerjean/filestash/server/common"
|
||||
. "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_mcp/types"
|
||||
)
|
||||
|
||||
func SendMessage(w io.Writer, requestID uint64, response any) {
|
||||
b, err := json.Marshal(JSONRPCResponse{
|
||||
JSONRPC: "2.0",
|
||||
ID: requestID,
|
||||
Result: &response,
|
||||
})
|
||||
if err != nil {
|
||||
SendError(w, requestID, JSONRPCError{
|
||||
Code: http.StatusInternalServerError,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
fmt.Fprintf(w, "event: message\ndata: %s\n\n", string(b))
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
func SendPing(w io.Writer, requestID uint64) {
|
||||
b, err := json.Marshal(JSONRPCRequest{
|
||||
JSONRPC: "2.0",
|
||||
ID: requestID,
|
||||
Method: "ping",
|
||||
})
|
||||
if err != nil {
|
||||
SendError(w, requestID, JSONRPCError{
|
||||
Code: http.StatusInternalServerError,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
fmt.Fprintf(w, "event: message\ndata: %s\n\n", string(b))
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
func SendMethod(w io.Writer, requestID uint64, method string, args ...map[string]any) {
|
||||
var params map[string]any
|
||||
if len(args) == 1 {
|
||||
params = args[0]
|
||||
}
|
||||
|
||||
b, err := json.Marshal(JSONRPCMethod{
|
||||
JSONRPC: "2.0",
|
||||
Method: method,
|
||||
Params: params,
|
||||
})
|
||||
if err != nil {
|
||||
SendError(w, requestID, JSONRPCError{
|
||||
Code: http.StatusInternalServerError,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
fmt.Fprintf(w, "event: message\ndata: %s\n\n", string(b))
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
func SendError(w io.Writer, requestID uint64, d error) {
|
||||
var rpcErr JSONRPCError
|
||||
switch v := d.(type) {
|
||||
case JSONRPCError:
|
||||
rpcErr = v
|
||||
case AppError:
|
||||
rpcErr = JSONRPCError{
|
||||
Code: v.Status(),
|
||||
Message: v.Error(),
|
||||
}
|
||||
default:
|
||||
rpcErr = JSONRPCError{
|
||||
Code: http.StatusInternalServerError,
|
||||
Message: d.Error(),
|
||||
}
|
||||
}
|
||||
b, err := json.Marshal(JSONRPCResponse{
|
||||
JSONRPC: "2.0",
|
||||
ID: requestID,
|
||||
Error: &rpcErr,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "event: message\ndata: %s\n\n", string(`nil`))
|
||||
} else {
|
||||
fmt.Fprintf(w, "event: message\ndata: %s\n\n", string(b))
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
Loading…
Reference in a new issue