feat: validate receive files list args
This commit is contained in:
@@ -14,12 +14,16 @@ import (
|
||||
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"`
|
||||
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"`
|
||||
OutputDir string `json:"output_dir,omitempty" jsonschema:"download output directory; not accepted in current list-only mode"`
|
||||
Cursor string `json:"cursor,omitempty" jsonschema:"pagination cursor; currently only empty cursor is supported"`
|
||||
SourcePreference string `json:"source_preference,omitempty" jsonschema:"source selector; currently supports auto or local_readonly only"`
|
||||
Preview bool `json:"preview,omitempty" jsonschema:"accepted for contract compatibility in list mode"`
|
||||
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) {
|
||||
@@ -39,6 +43,12 @@ func RegisterISphereFileTools(server *mcp.Server, source ReceiveMessagesSource)
|
||||
if mode != "list" {
|
||||
return nil, nil, fmt.Errorf("isphere_receive_files mode %q is not available; only list is supported", input.Mode)
|
||||
}
|
||||
if err := validateLocalReadonlySourceAndCursor(ToolNameReceiveFiles, input.SourcePreference, input.Cursor); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if strings.TrimSpace(input.OutputDir) != "" {
|
||||
return nil, nil, fmt.Errorf("isphere_receive_files output_dir is only available for download mode, which is not implemented yet")
|
||||
}
|
||||
messages, err := source.ReceiveMessages(ctx, isphere.ReceiveMessagesQuery{Limit: 0})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
||||
@@ -88,3 +88,70 @@ func TestISphereReceiveFilesToolReturnsLogBackedFiles(t *testing.T) {
|
||||
t.Fatalf("source queries = %+v, want one unbounded receive query", fake.queries)
|
||||
}
|
||||
}
|
||||
|
||||
func TestISphereReceiveFilesToolValidatesContractArgs(t *testing.T) {
|
||||
fake := &fakeReceiveMessagesSource{
|
||||
result: isphere.ReceiveMessagesResult{Messages: []isphere.Message{{
|
||||
ID: "msg-file-1",
|
||||
ConversationID: "alice@imopenfire1-lanzhou|bob@imopenfire1-lanzhou",
|
||||
Text: "redacted-report.docx",
|
||||
Timestamp: "1783423807000",
|
||||
Subject: "FILE_TRANSFER_CANCEL",
|
||||
}}},
|
||||
}
|
||||
session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) {
|
||||
RegisterISphereFileTools(server, fake)
|
||||
})
|
||||
defer cleanup()
|
||||
|
||||
callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
|
||||
Name: ToolNameReceiveFiles,
|
||||
Arguments: map[string]any{
|
||||
"mode": "list",
|
||||
"name_contains": "report",
|
||||
"source_preference": "local_readonly",
|
||||
"preview": true,
|
||||
"cursor": "",
|
||||
"output_dir": "",
|
||||
"limit": 5,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("call with safe contract args: %v", err)
|
||||
}
|
||||
if callResult.IsError {
|
||||
t.Fatalf("call result is error: %+v", callResult)
|
||||
}
|
||||
|
||||
bridgeResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
|
||||
Name: ToolNameReceiveFiles,
|
||||
Arguments: map[string]any{"mode": "list", "source_preference": "bridge"},
|
||||
})
|
||||
if err == nil && !bridgeResult.IsError {
|
||||
t.Fatalf("bridge source_preference was accepted before connector exists")
|
||||
}
|
||||
|
||||
cursorResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
|
||||
Name: ToolNameReceiveFiles,
|
||||
Arguments: map[string]any{"mode": "list", "cursor": "next-page"},
|
||||
})
|
||||
if err == nil && !cursorResult.IsError {
|
||||
t.Fatalf("non-empty cursor was accepted before pagination exists")
|
||||
}
|
||||
|
||||
outputResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
|
||||
Name: ToolNameReceiveFiles,
|
||||
Arguments: map[string]any{"mode": "list", "output_dir": "C:\\tmp\\isphere"},
|
||||
})
|
||||
if err == nil && !outputResult.IsError {
|
||||
t.Fatalf("output_dir was accepted in list mode before download exists")
|
||||
}
|
||||
|
||||
downloadResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
|
||||
Name: ToolNameReceiveFiles,
|
||||
Arguments: map[string]any{"mode": "download", "file_id": "msg-file-1:redacted-report.docx"},
|
||||
})
|
||||
if err == nil && !downloadResult.IsError {
|
||||
t.Fatalf("download mode was accepted before cache mapping exists")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user