feat(mcp): expose create_attachment tool
Add AttachmentService_CreateAttachment to the curated MCP allowlist so agents can upload files (inline base64 content) alongside memos, closing the gap where the MCP server could list/get/delete attachments but not create them. Closes #6057
This commit is contained in:
@@ -154,6 +154,7 @@ resolve its own user — the single allowed auth/identity operation):
|
|||||||
| `MemoService_ListMemoRelations` | `memo_list_memo_relations` |
|
| `MemoService_ListMemoRelations` | `memo_list_memo_relations` |
|
||||||
| `MemoService_SetMemoRelations` | `memo_set_memo_relations` |
|
| `MemoService_SetMemoRelations` | `memo_set_memo_relations` |
|
||||||
| `AttachmentService_ListAttachments` | `attachment_list_attachments` |
|
| `AttachmentService_ListAttachments` | `attachment_list_attachments` |
|
||||||
|
| `AttachmentService_CreateAttachment` | `attachment_create_attachment` |
|
||||||
| `AttachmentService_GetAttachment` | `attachment_get_attachment` |
|
| `AttachmentService_GetAttachment` | `attachment_get_attachment` |
|
||||||
| `AttachmentService_DeleteAttachment` | `attachment_delete_attachment` |
|
| `AttachmentService_DeleteAttachment` | `attachment_delete_attachment` |
|
||||||
| `ShortcutService_ListShortcuts` | `shortcut_list_shortcuts` |
|
| `ShortcutService_ListShortcuts` | `shortcut_list_shortcuts` |
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ var curatedOperationIDs = []string{
|
|||||||
"MemoService_ListMemoRelations",
|
"MemoService_ListMemoRelations",
|
||||||
"MemoService_SetMemoRelations",
|
"MemoService_SetMemoRelations",
|
||||||
"AttachmentService_ListAttachments",
|
"AttachmentService_ListAttachments",
|
||||||
|
"AttachmentService_CreateAttachment",
|
||||||
"AttachmentService_GetAttachment",
|
"AttachmentService_GetAttachment",
|
||||||
"AttachmentService_DeleteAttachment",
|
"AttachmentService_DeleteAttachment",
|
||||||
"ShortcutService_ListShortcuts",
|
"ShortcutService_ListShortcuts",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestCuratedOperationIDsStayMemoFocused(t *testing.T) {
|
func TestCuratedOperationIDsStayMemoFocused(t *testing.T) {
|
||||||
require.Len(t, curatedOperationIDs, 19)
|
require.Len(t, curatedOperationIDs, 20)
|
||||||
|
|
||||||
for _, operationID := range curatedOperationIDs {
|
for _, operationID := range curatedOperationIDs {
|
||||||
require.NotContains(t, operationID, "Admin")
|
require.NotContains(t, operationID, "Admin")
|
||||||
@@ -104,6 +104,42 @@ func TestBuildToolFromOperationIncludesRequestBodySchema(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildToolFromOperationExposesCreateAttachment(t *testing.T) {
|
||||||
|
spec, err := loadOpenAPISpec("../../../proto/gen/openapi.yaml")
|
||||||
|
require.NoError(t, err)
|
||||||
|
registry, err := buildOperationRegistry(spec)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tool, operation := buildToolFromOperation(registry["AttachmentService_CreateAttachment"])
|
||||||
|
require.Equal(t, "attachment_create_attachment", tool.Name)
|
||||||
|
require.Equal(t, "POST", operation.Method)
|
||||||
|
require.False(t, tool.Annotations.ReadOnlyHint)
|
||||||
|
require.False(t, *tool.Annotations.DestructiveHint)
|
||||||
|
require.False(t, tool.Annotations.IdempotentHint)
|
||||||
|
|
||||||
|
input, ok := tool.InputSchema.(jsonSchema)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Contains(t, input["required"], "body")
|
||||||
|
properties, ok := input["properties"].(map[string]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
// attachmentId is an optional query parameter; the file itself is the body.
|
||||||
|
require.Contains(t, properties, "attachmentId")
|
||||||
|
require.Contains(t, properties, "body")
|
||||||
|
body, ok := properties["body"].(jsonSchema)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Contains(t, body["properties"], "filename")
|
||||||
|
require.Contains(t, body["properties"], "content")
|
||||||
|
|
||||||
|
err = validateToolArguments(input, map[string]any{
|
||||||
|
"body": map[string]any{
|
||||||
|
"filename": "screenshot.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"content": "aGVsbG8=",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildToolFromOperationExposesCurrentUser(t *testing.T) {
|
func TestBuildToolFromOperationExposesCurrentUser(t *testing.T) {
|
||||||
spec, err := loadOpenAPISpec("../../../proto/gen/openapi.yaml")
|
spec, err := loadOpenAPISpec("../../../proto/gen/openapi.yaml")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
Reference in New Issue
Block a user