From 3819eb4feed637e5032411e1b4b97bd1a970aec6 Mon Sep 17 00:00:00 2001 From: "Hein (Warky)" Date: Tue, 31 Mar 2026 00:20:36 +0200 Subject: [PATCH] feat(files): update save_file tool description and enforce size limit for base64 payloads --- internal/mcpserver/schema.go | 16 +++++++++++++++- internal/mcpserver/server.go | 2 +- internal/tools/files.go | 10 ++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/internal/mcpserver/schema.go b/internal/mcpserver/schema.go index ee88a3c..c402b6f 100644 --- a/internal/mcpserver/schema.go +++ b/internal/mcpserver/schema.go @@ -2,6 +2,7 @@ package mcpserver import ( "context" + "encoding/json" "fmt" "log/slog" "reflect" @@ -12,6 +13,8 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" ) +const maxLoggedArgBytes = 512 + var toolSchemaOptions = &jsonschema.ForOptions{ TypeSchemas: map[reflect.Type]*jsonschema.Schema{ reflect.TypeFor[uuid.UUID](): { @@ -37,7 +40,7 @@ func logToolCall[In any, Out any](logger *slog.Logger, toolName string, handler start := time.Now() attrs := []any{slog.String("tool", toolName)} if req != nil && req.Params != nil { - attrs = append(attrs, slog.Any("arguments", req.Params.Arguments)) + attrs = append(attrs, slog.String("arguments", truncateArgs(req.Params.Arguments))) } logger.Info("mcp tool started", attrs...) @@ -56,6 +59,17 @@ func logToolCall[In any, Out any](logger *slog.Logger, toolName string, handler } } +func truncateArgs(args any) string { + b, err := json.Marshal(args) + if err != nil { + return "" + } + if len(b) <= maxLoggedArgBytes { + return string(b) + } + return string(b[:maxLoggedArgBytes]) + fmt.Sprintf("… (%d bytes total)", len(b)) +} + func setToolSchemas[In any, Out any](tool *mcp.Tool) error { if tool.InputSchema == nil { inputSchema, err := jsonschema.For[In](toolSchemaOptions) diff --git a/internal/mcpserver/server.go b/internal/mcpserver/server.go index aac65fd..ed7d3f7 100644 --- a/internal/mcpserver/server.go +++ b/internal/mcpserver/server.go @@ -136,7 +136,7 @@ func New(cfg config.MCPConfig, logger *slog.Logger, toolSet ToolSet) http.Handle addTool(server, logger, &mcp.Tool{ Name: "save_file", - Description: "Store a base64-encoded file such as an image, document, or audio clip, optionally linking it to a thought.", + Description: "Store a file and optionally link it to a thought. Supply either content_base64 (≤10 MB) or content_uri (amcs://files/{id} from a prior POST /files upload). For files larger than 10 MB, upload via POST /files first and pass the returned URI as content_uri.", }, toolSet.Files.Save) addTool(server, logger, &mcp.Tool{ diff --git a/internal/tools/files.go b/internal/tools/files.go index 37b9655..181cda4 100644 --- a/internal/tools/files.go +++ b/internal/tools/files.go @@ -16,6 +16,11 @@ import ( thoughttypes "git.warky.dev/wdevs/amcs/internal/types" ) +// maxBase64ToolBytes is the maximum base64 payload accepted by save_file via +// the MCP tool interface. For larger files use POST /files (binary) and pass +// the returned amcs://files/{id} URI as content_uri instead. +const maxBase64ToolBytes = 10 << 20 // 10 MB of base64 ≈ 7.5 MB decoded + type FilesTool struct { store *store.DB sessions *session.ActiveProjects @@ -75,6 +80,11 @@ func (t *FilesTool) Save(ctx context.Context, req *mcp.CallToolRequest, in SaveF if uri != "" && b64 != "" { return nil, SaveFileOutput{}, errInvalidInput("provide content_uri or content_base64, not both") } + if len(b64) > maxBase64ToolBytes { + return nil, SaveFileOutput{}, errInvalidInput( + "content_base64 exceeds the 10 MB MCP tool limit; upload the file via POST /files and pass the returned amcs://files/{id} URI as content_uri instead", + ) + } var content []byte var mediaTypeFromSource string