package tools import ( "context" "fmt" "strings" "time" "github.com/modelcontextprotocol/go-sdk/mcp" "isphere-ai-bridge/internal/isphere" ) const ToolNameReceiveFiles = "isphere_receive_files" type ReceiveFilesArgs struct { ConversationID string `json:"conversation_id,omitempty" jsonschema:"conversation id filter for file listing"` MessageID string `json:"message_id,omitempty" jsonschema:"message id filter for file listing"` FileID string `json:"file_id,omitempty" jsonschema:"file id filter for file listing"` NameContains string `json:"name_contains,omitempty" jsonschema:"case-insensitive filename substring filter"` Mode string `json:"mode,omitempty" jsonschema:"file receive mode; C14 supports list only"` Limit int `json:"limit,omitempty" jsonschema:"maximum number of files to return; zero or negative returns all matches"` } func RegisterISphereFileTools(server *mcp.Server, source ReceiveMessagesSource) { if source == nil { source = isphere.EncryptedPacketLogSource{} } mcp.AddTool[ReceiveFilesArgs, map[string]any](server, &mcp.Tool{ Name: ToolNameReceiveFiles, Description: "List iSphere file metadata candidates from the configured read-only message source; download is deferred until cache mapping is validated.", }, func(ctx context.Context, _ *mcp.CallToolRequest, input ReceiveFilesArgs) (*mcp.CallToolResult, map[string]any, error) { started := time.Now().UTC() mode := strings.ToLower(strings.TrimSpace(input.Mode)) if mode == "" { mode = "list" } if mode != "list" { return nil, nil, fmt.Errorf("isphere_receive_files mode %q is not available; only list is supported", input.Mode) } messages, err := source.ReceiveMessages(ctx, isphere.ReceiveMessagesQuery{Limit: 0}) if err != nil { return nil, nil, err } files := isphere.ListFilesFromMessages(messages.Messages, isphere.ListFilesQuery{ ConversationID: input.ConversationID, MessageID: input.MessageID, FileID: input.FileID, NameContains: input.NameContains, Limit: input.Limit, }) return nil, receiveFilesResultToMap(files, started, time.Now().UTC()), nil }) } func receiveFilesResultToMap(result isphere.ListFilesResult, started time.Time, finished time.Time) map[string]any { files := make([]map[string]any, 0, len(result.Files)) for _, file := range result.Files { files = append(files, map[string]any{ "file_id": file.FileID, "message_id": nullableString(file.MessageID), "conversation_id": nullableString(file.ConversationID), "file_name": file.FileName, "size_bytes": file.SizeBytes, "mime_type": file.MimeType, "created_at": nullableString(file.CreatedAt), "download_ref": nullableString(file.DownloadRef), "saved_path": nullableString(file.SavedPath), "sha256": nullableString(file.SHA256), "source": file.Source, "raw_ref": file.RawRef, }) } return map[string]any{ "ok": true, "mode": "list", "files": files, "next_cursor": nil, "audit": map[string]any{ "tool": ToolNameReceiveFiles, "source": "local_readonly", "execution_mode": "read", "started_at": started.Format(time.RFC3339Nano), "finished_at": finished.Format(time.RFC3339Nano), "result": "ok", }, } } func nullableString(value string) any { if strings.TrimSpace(value) == "" { return nil } return value }