feat(files): introduce upload_file tool for staging files and enhance save_file documentation
This commit is contained in:
@@ -6,6 +6,8 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -65,6 +67,21 @@ type ListFilesInput struct {
|
||||
Kind string `json:"kind,omitempty" jsonschema:"optional kind filter such as image, document, audio, or file"`
|
||||
}
|
||||
|
||||
type UploadFileInput struct {
|
||||
Name string `json:"name" jsonschema:"file name including extension, for example photo.png or note.pdf"`
|
||||
ContentPath string `json:"content_path,omitempty" jsonschema:"absolute path to a file on the server; preferred for large files — no base64 overhead"`
|
||||
ContentBase64 string `json:"content_base64,omitempty" jsonschema:"file contents encoded as base64 (≤10 MB); use content_path for larger files"`
|
||||
MediaType string `json:"media_type,omitempty" jsonschema:"optional MIME type such as image/png, application/pdf, or audio/mpeg"`
|
||||
Kind string `json:"kind,omitempty" jsonschema:"optional logical type such as image, document, audio, or file"`
|
||||
ThoughtID string `json:"thought_id,omitempty" jsonschema:"optional thought id to link this file to immediately"`
|
||||
Project string `json:"project,omitempty" jsonschema:"optional project name or id"`
|
||||
}
|
||||
|
||||
type UploadFileOutput struct {
|
||||
File thoughttypes.StoredFile `json:"file"`
|
||||
URI string `json:"uri" jsonschema:"amcs resource URI for this file, e.g. amcs://files/{id}; pass as content_uri in save_file to link without re-uploading"`
|
||||
}
|
||||
|
||||
type ListFilesOutput struct {
|
||||
Files []thoughttypes.StoredFile `json:"files"`
|
||||
}
|
||||
@@ -73,6 +90,60 @@ func NewFilesTool(db *store.DB, sessions *session.ActiveProjects) *FilesTool {
|
||||
return &FilesTool{store: db, sessions: sessions}
|
||||
}
|
||||
|
||||
func (t *FilesTool) Upload(ctx context.Context, req *mcp.CallToolRequest, in UploadFileInput) (*mcp.CallToolResult, UploadFileOutput, error) {
|
||||
path := strings.TrimSpace(in.ContentPath)
|
||||
b64 := strings.TrimSpace(in.ContentBase64)
|
||||
|
||||
if path != "" && b64 != "" {
|
||||
return nil, UploadFileOutput{}, errInvalidInput("provide content_path or content_base64, not both")
|
||||
}
|
||||
|
||||
var content []byte
|
||||
var mediaTypeFromSource string
|
||||
|
||||
if path != "" {
|
||||
if !filepath.IsAbs(path) {
|
||||
return nil, UploadFileOutput{}, errInvalidInput("content_path must be an absolute path")
|
||||
}
|
||||
var err error
|
||||
content, err = os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, UploadFileOutput{}, errInvalidInput("cannot read content_path: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
if b64 == "" {
|
||||
return nil, UploadFileOutput{}, errInvalidInput("content_path or content_base64 is required")
|
||||
}
|
||||
if len(b64) > maxBase64ToolBytes {
|
||||
return nil, UploadFileOutput{}, errInvalidInput(
|
||||
"content_base64 exceeds the 10 MB MCP tool limit; use content_path instead",
|
||||
)
|
||||
}
|
||||
raw, dataURLMediaType := splitDataURL(b64)
|
||||
var err error
|
||||
content, err = decodeBase64(raw)
|
||||
if err != nil {
|
||||
return nil, UploadFileOutput{}, errInvalidInput("content_base64 must be valid base64")
|
||||
}
|
||||
mediaTypeFromSource = dataURLMediaType
|
||||
}
|
||||
|
||||
out, err := t.SaveDecoded(ctx, req, SaveFileDecodedInput{
|
||||
Name: in.Name,
|
||||
Content: content,
|
||||
MediaType: firstNonEmpty(strings.TrimSpace(in.MediaType), mediaTypeFromSource),
|
||||
Kind: in.Kind,
|
||||
ThoughtID: in.ThoughtID,
|
||||
Project: in.Project,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, UploadFileOutput{}, err
|
||||
}
|
||||
|
||||
uri := fileURIPrefix + out.File.ID.String()
|
||||
return nil, UploadFileOutput{File: out.File, URI: uri}, nil
|
||||
}
|
||||
|
||||
func (t *FilesTool) Save(ctx context.Context, req *mcp.CallToolRequest, in SaveFileInput) (*mcp.CallToolResult, SaveFileOutput, error) {
|
||||
uri := strings.TrimSpace(in.ContentURI)
|
||||
b64 := strings.TrimSpace(in.ContentBase64)
|
||||
@@ -338,6 +409,12 @@ func decodeBase64(value string) ([]byte, error) {
|
||||
}
|
||||
}, value)
|
||||
|
||||
var candidates []string
|
||||
candidates = append(candidates, cleaned)
|
||||
if trimmed := strings.TrimRight(cleaned, "="); trimmed != cleaned && trimmed != "" {
|
||||
candidates = append(candidates, trimmed)
|
||||
}
|
||||
|
||||
encodings := []*base64.Encoding{
|
||||
base64.StdEncoding,
|
||||
base64.RawStdEncoding,
|
||||
@@ -346,12 +423,14 @@ func decodeBase64(value string) ([]byte, error) {
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for _, encoding := range encodings {
|
||||
decoded, err := encoding.DecodeString(cleaned)
|
||||
if err == nil {
|
||||
return decoded, nil
|
||||
for _, candidate := range candidates {
|
||||
for _, encoding := range encodings {
|
||||
decoded, err := encoding.DecodeString(candidate)
|
||||
if err == nil {
|
||||
return decoded, nil
|
||||
}
|
||||
lastErr = err
|
||||
}
|
||||
lastErr = err
|
||||
}
|
||||
|
||||
return nil, lastErr
|
||||
|
||||
Reference in New Issue
Block a user