diff --git a/CHANGELOG.md b/CHANGELOG.md index c2c78c6..ccfcb72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Aligned CLI flag types, values and validation with what the public API actually accepts: `codes --max-redemptions 0` / `--expire-after 0` now send `null` (unlimited / clear) rather than 422; `maps paths --cost` is an integer and `--cost-override` a boolean; `maps paths --direction` accepts `bidirectional | forward | backward`; `maps layers features create` requires `--name`; `collections items --radius` is guarded `>= 1`; `screens sections prices`/`hours` honour `--lang` for their translated fields; `CrossRegionLink` is rejected client-side for collection-item and link-item types; media types validate through one shared `api.ValidateMediaType`; and `codes list --sort-field` no longer advertises the unsupported `redemptions`. +- Corrected MCP tool contracts: `list_collection_items` returns its rows under a `collection_items` key (was a generic `items`); `create_collection`/`screen`/`section`/`code`/`media`/`map_feature` document their server-required fields; pagination params are described consistently (server default 30, max 1000) instead of four contradictory phrasings; `delete_media` gained a per-locale `language` arg; and `update_collection`/`update_screen` note the `primary_language`-flip caveat. ## [0.10.33] - 2026-05-28 diff --git a/internal/mcp/server_test.go b/internal/mcp/server_test.go index 340e52a..be4edf1 100644 --- a/internal/mcp/server_test.go +++ b/internal/mcp/server_test.go @@ -246,7 +246,7 @@ func TestResolveClientNoConfigError(t *testing.T) { orig, _ := os.Getwd() defer os.Chdir(orig) os.Chdir(dir) - + t.Setenv("STQRY_CONFIG_HOME", dir) _, err := stqrymcp.ResolveClient(nil) @@ -286,6 +286,46 @@ func TestResolveClientSessionPriorityOverDisk(t *testing.T) { } } +// ---- list_collection_items ---- + +// TestListCollectionItemsKey locks in the wrapper-key fix: list_collection_items +// must return its rows under "collection_items" (the server's response key and +// the convention every other list tool follows), not a generic "items". +func TestListCollectionItemsKey(t *testing.T) { + mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/public/collections/42/collection_items" { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{"collection_items":[{"id":"7","item_type":"Screen","item_id":"99"}],"meta":{"page":1,"pages":1,"per_page":30,"count":1}}`) + })) + defer mock.Close() + + dir := t.TempDir() + cfg := &config.DirectoryConfig{APIKey: "tok", APIURL: mock.URL} + if err := config.SaveDirectoryConfig(dir, cfg); err != nil { + t.Fatal(err) + } + orig, _ := os.Getwd() + defer os.Chdir(orig) + os.Chdir(dir) + + s := stqrymcp.NewServer() + result := callTool(s, "list_collection_items", `{"collection_id":"42"}`) + if result == nil || result.IsError { + t.Fatalf("expected success, got: %v", result) + } + txt := toolText(result) + if !strings.Contains(txt, `"collection_items"`) { + t.Errorf("expected rows under \"collection_items\" key, got: %s", txt) + } + // The lone bare-"items" key was the bug; make sure it didn't come back. + if strings.Contains(txt, `"items"`) { + t.Errorf("did not expect a bare \"items\" key, got: %s", txt) + } +} + // ---- list_projects ---- func TestListProjectsHappyPath(t *testing.T) { @@ -637,8 +677,8 @@ func TestCreateMediaInvalidType(t *testing.T) { if !result.IsError { t.Fatal("expected error for invalid type") } - if !strings.Contains(toolText(result), "invalid type") { - t.Errorf("expected helpful error mentioning invalid type, got: %s", toolText(result)) + if !strings.Contains(toolText(result), "invalid media type") { + t.Errorf("expected helpful error mentioning invalid media type, got: %s", toolText(result)) } } @@ -652,6 +692,25 @@ func TestCreateMediaMissingFilePath(t *testing.T) { } } +func TestCreateMediaMissingName(t *testing.T) { + // MediaItem requires a name for every subtype and the public API derives + // none from the filename, so create_media must reject a blank name + // client-side (before uploading) rather than orphan a file and 422. + dir := t.TempDir() + filePath := filepath.Join(dir, "f.mp4") + if err := os.WriteFile(filePath, []byte("x"), 0600); err != nil { + t.Fatal(err) + } + s := stqrymcp.NewServer() + result := callTool(s, "create_media", fmt.Sprintf(`{"file_path":%q,"type":"video"}`, filePath)) + if result == nil || !result.IsError { + t.Fatal("expected error for missing name") + } + if !strings.Contains(toolText(result), "name is required") { + t.Errorf("expected 'name is required' error, got: %s", toolText(result)) + } +} + func TestCreateMediaMissingType(t *testing.T) { dir := t.TempDir() filePath := filepath.Join(dir, "f.mp4") @@ -678,7 +737,7 @@ func TestCreateMediaBadFilePath(t *testing.T) { os.Chdir(dir) s := stqrymcp.NewServer() - result := callTool(s, "create_media", `{"file_path":"/nonexistent/file.mp4","type":"video"}`) + result := callTool(s, "create_media", `{"file_path":"/nonexistent/file.mp4","type":"video","name":"Test"}`) if result == nil || !result.IsError { t.Fatal("expected error for non-existent file path") } @@ -796,9 +855,8 @@ func TestCreateMediaUploadError(t *testing.T) { os.Chdir(dir) s := stqrymcp.NewServer() - result := callTool(s, "create_media", fmt.Sprintf(`{"file_path":%q,"type":"video"}`, filePath)) + result := callTool(s, "create_media", fmt.Sprintf(`{"file_path":%q,"type":"video","name":"Test"}`, filePath)) if result == nil || !result.IsError { t.Fatal("expected error when upload API returns 500") } } - diff --git a/internal/mcp/tools_codes.go b/internal/mcp/tools_codes.go index ea27d02..4ec965d 100644 --- a/internal/mcp/tools_codes.go +++ b/internal/mcp/tools_codes.go @@ -14,12 +14,8 @@ func registerCodeTools(s *server.MCPServer, sess *Session) { s.AddTool( mcpgo.NewTool("list_codes", mcpgo.WithDescription("List all codes for the configured STQRY account."), - mcpgo.WithNumber("page", - mcpgo.Description("Page number (1-based, default: 1)"), - ), - mcpgo.WithNumber("per_page", - mcpgo.Description("Items per page (default: 25)"), - ), + pageParam(), + perPageParam(), ), func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) { client, err := ResolveClient(sess) @@ -66,10 +62,10 @@ func registerCodeTools(s *server.MCPServer, sess *Session) { // create_code: creates a new code s.AddTool( mcpgo.NewTool("create_code", - mcpgo.WithDescription("Create a new STQRY code."), + mcpgo.WithDescription("Create a new STQRY code. Required: coupon_code, linked_type, linked_id, project_id. valid_from and valid_to must be set together (setting one without the other is rejected)."), mcpgo.WithObject("fields", mcpgo.Required(), - mcpgo.Description("Arbitrary JSON object of code fields to set"), + mcpgo.Description("Code fields. Required: coupon_code, linked_type (the linked resource's type, e.g. Collection), linked_id, project_id. Optional: valid_from + valid_to (set both or neither), max_redemptions (integer >= 1; OMIT or send null for unlimited — a literal 0 is rejected), expire_after, timezone, tags. Example: {\"coupon_code\": \"SUMMER\", \"linked_type\": \"Collection\", \"linked_id\": 42, \"project_id\": 7}."), ), ), func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) { diff --git a/internal/mcp/tools_collections.go b/internal/mcp/tools_collections.go index f027632..93c4ec2 100644 --- a/internal/mcp/tools_collections.go +++ b/internal/mcp/tools_collections.go @@ -14,12 +14,8 @@ func registerCollectionTools(s *server.MCPServer, sess *Session) { s.AddTool( mcpgo.NewTool("list_collections", mcpgo.WithDescription("List all collections for the configured STQRY account."), - mcpgo.WithNumber("page", - mcpgo.Description("Page number (1-based, default: 1)"), - ), - mcpgo.WithNumber("per_page", - mcpgo.Description("Items per page (default: 25)"), - ), + pageParam(), + perPageParam(), ), func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) { client, err := ResolveClient(sess) @@ -66,10 +62,10 @@ func registerCollectionTools(s *server.MCPServer, sess *Session) { // create_collection: creates a new collection with the given fields s.AddTool( mcpgo.NewTool("create_collection", - mcpgo.WithDescription("Create a new STQRY collection."), + mcpgo.WithDescription("Create a new STQRY collection. Required: type (one of: list, tour, organization, menu, search), name, and primary_language (e.g. \"en\"). The server also requires title and short_title on the primary language; pass translated fields as locale maps, e.g. {\"en\": \"…\"}."), mcpgo.WithObject("fields", mcpgo.Required(), - mcpgo.Description("Arbitrary JSON object of collection fields to set"), + mcpgo.Description("Collection fields. Required: type, name, primary_language, plus title and short_title on the primary language. Example: {\"type\": \"tour\", \"name\": \"City Walk\", \"primary_language\": \"en\", \"title\": {\"en\": \"City Walk\"}, \"short_title\": {\"en\": \"Walk\"}}. Optional: description, tour_type, the *_media_item_id covers/logo/preview, map_view_enabled."), ), ), func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) { @@ -93,7 +89,7 @@ func registerCollectionTools(s *server.MCPServer, sess *Session) { // update_collection: updates an existing collection by ID s.AddTool( mcpgo.NewTool("update_collection", - mcpgo.WithDescription("Update an existing STQRY collection by ID."), + mcpgo.WithDescription("Update an existing STQRY collection by ID. Translated fields (title, short_title, description) are locale maps, e.g. {\"en\": \"…\"}. Pass primary_language to flip the primary locale — the target locale must already have title + short_title populated, or the server rejects the flip."), mcpgo.WithString("id", mcpgo.Required(), mcpgo.Description("The collection ID"), @@ -165,12 +161,8 @@ func registerCollectionTools(s *server.MCPServer, sess *Session) { mcpgo.Required(), mcpgo.Description("The collection ID"), ), - mcpgo.WithNumber("page", - mcpgo.Description("Page number (1-based, default: 1)"), - ), - mcpgo.WithNumber("per_page", - mcpgo.Description("Items per page (default: 25)"), - ), + pageParam(), + perPageParam(), ), func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) { collectionID := req.GetString("collection_id", "") @@ -186,8 +178,8 @@ func registerCollectionTools(s *server.MCPServer, sess *Session) { return mcpgo.NewToolResultError(fmt.Sprintf("listing collection items: %v", err)), nil } return jsonResult(map[string]interface{}{ - "items": items, - "meta": meta, + "collection_items": items, + "meta": meta, }) }, ) diff --git a/internal/mcp/tools_maps.go b/internal/mcp/tools_maps.go index 8ea38f3..0453733 100644 --- a/internal/mcp/tools_maps.go +++ b/internal/mcp/tools_maps.go @@ -21,8 +21,8 @@ func registerMapCRUD(s *server.MCPServer, sess *Session) { s.AddTool( mcpgo.NewTool("list_maps", mcpgo.WithDescription("List all maps in the configured STQRY account."), - mcpgo.WithNumber("page", mcpgo.Description("Page number (default 1)")), - mcpgo.WithNumber("per_page", mcpgo.Description("Items per page (default 25)")), + pageParam(), + perPageParam(), mcpgo.WithString("q", mcpgo.Description("Search query (matches map name)")), ), func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) { @@ -200,9 +200,15 @@ func registerMapNestedCRUD(s *server.MCPServer, sess *Session) { } return map[string]interface{}{"map_layers": items, "meta": meta}, nil }, - get: func(c *api.Client, mapID, id string) (map[string]interface{}, error) { return api.GetMapLayer(c, mapID, id) }, - create: func(c *api.Client, mapID string, fields map[string]interface{}) (map[string]interface{}, error) { return api.CreateMapLayer(c, mapID, fields) }, - update: func(c *api.Client, mapID, id string, fields map[string]interface{}) (map[string]interface{}, error) { return api.UpdateMapLayer(c, mapID, id, fields) }, + get: func(c *api.Client, mapID, id string) (map[string]interface{}, error) { + return api.GetMapLayer(c, mapID, id) + }, + create: func(c *api.Client, mapID string, fields map[string]interface{}) (map[string]interface{}, error) { + return api.CreateMapLayer(c, mapID, fields) + }, + update: func(c *api.Client, mapID, id string, fields map[string]interface{}) (map[string]interface{}, error) { + return api.UpdateMapLayer(c, mapID, id, fields) + }, delete: func(c *api.Client, mapID, id string) error { return api.DeleteMapLayer(c, mapID, id) }, }) @@ -247,9 +253,15 @@ func registerMapNestedCRUD(s *server.MCPServer, sess *Session) { } return map[string]interface{}{"map_pois": items, "meta": meta}, nil }, - get: func(c *api.Client, mapID, id string) (map[string]interface{}, error) { return api.GetMapPoi(c, mapID, id) }, - create: func(c *api.Client, mapID string, fields map[string]interface{}) (map[string]interface{}, error) { return api.CreateMapPoi(c, mapID, fields) }, - update: func(c *api.Client, mapID, id string, fields map[string]interface{}) (map[string]interface{}, error) { return api.UpdateMapPoi(c, mapID, id, fields) }, + get: func(c *api.Client, mapID, id string) (map[string]interface{}, error) { + return api.GetMapPoi(c, mapID, id) + }, + create: func(c *api.Client, mapID string, fields map[string]interface{}) (map[string]interface{}, error) { + return api.CreateMapPoi(c, mapID, fields) + }, + update: func(c *api.Client, mapID, id string, fields map[string]interface{}) (map[string]interface{}, error) { + return api.UpdateMapPoi(c, mapID, id, fields) + }, delete: func(c *api.Client, mapID, id string) error { return api.DeleteMapPoi(c, mapID, id) }, }) @@ -264,9 +276,15 @@ func registerMapNestedCRUD(s *server.MCPServer, sess *Session) { } return map[string]interface{}{"map_path_nodes": items, "meta": meta}, nil }, - get: func(c *api.Client, mapID, id string) (map[string]interface{}, error) { return api.GetMapPathNode(c, mapID, id) }, - create: func(c *api.Client, mapID string, fields map[string]interface{}) (map[string]interface{}, error) { return api.CreateMapPathNode(c, mapID, fields) }, - update: func(c *api.Client, mapID, id string, fields map[string]interface{}) (map[string]interface{}, error) { return api.UpdateMapPathNode(c, mapID, id, fields) }, + get: func(c *api.Client, mapID, id string) (map[string]interface{}, error) { + return api.GetMapPathNode(c, mapID, id) + }, + create: func(c *api.Client, mapID string, fields map[string]interface{}) (map[string]interface{}, error) { + return api.CreateMapPathNode(c, mapID, fields) + }, + update: func(c *api.Client, mapID, id string, fields map[string]interface{}) (map[string]interface{}, error) { + return api.UpdateMapPathNode(c, mapID, id, fields) + }, delete: func(c *api.Client, mapID, id string) error { return api.DeleteMapPathNode(c, mapID, id) }, }) @@ -281,9 +299,15 @@ func registerMapNestedCRUD(s *server.MCPServer, sess *Session) { } return map[string]interface{}{"map_paths": items, "meta": meta}, nil }, - get: func(c *api.Client, mapID, id string) (map[string]interface{}, error) { return api.GetMapPath(c, mapID, id) }, - create: func(c *api.Client, mapID string, fields map[string]interface{}) (map[string]interface{}, error) { return api.CreateMapPath(c, mapID, fields) }, - update: func(c *api.Client, mapID, id string, fields map[string]interface{}) (map[string]interface{}, error) { return api.UpdateMapPath(c, mapID, id, fields) }, + get: func(c *api.Client, mapID, id string) (map[string]interface{}, error) { + return api.GetMapPath(c, mapID, id) + }, + create: func(c *api.Client, mapID string, fields map[string]interface{}) (map[string]interface{}, error) { + return api.CreateMapPath(c, mapID, fields) + }, + update: func(c *api.Client, mapID, id string, fields map[string]interface{}) (map[string]interface{}, error) { + return api.UpdateMapPath(c, mapID, id, fields) + }, delete: func(c *api.Client, mapID, id string) error { return api.DeleteMapPath(c, mapID, id) }, }) @@ -319,8 +343,8 @@ func registerMapNestedCRUD(s *server.MCPServer, sess *Session) { mcpgo.WithDescription("List GeoJSON features in a map layer."), mapIDArg, mcpgo.WithString("layer_id", mcpgo.Required(), mcpgo.Description("Layer ID")), - mcpgo.WithNumber("page", mcpgo.Description("Page number")), - mcpgo.WithNumber("per_page", mcpgo.Description("Items per page")), + pageParam(), + perPageParam(), ), func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) { mapID := req.GetString("map_id", "") @@ -369,10 +393,10 @@ func registerMapNestedCRUD(s *server.MCPServer, sess *Session) { s.AddTool( mcpgo.NewTool("create_map_feature", - mcpgo.WithDescription("Create a GeoJSON feature in a map layer. `fields` must include geojson; may include name, label, settings, map_poi_id."), + mcpgo.WithDescription("Create a GeoJSON feature in a map layer. `fields` must include geojson and name (both required server-side); may include label, settings, map_poi_id."), mapIDArg, mcpgo.WithString("layer_id", mcpgo.Required(), mcpgo.Description("Layer ID")), - mcpgo.WithObject("fields", mcpgo.Required(), mcpgo.Description("Feature fields including geojson")), + mcpgo.WithObject("fields", mcpgo.Required(), mcpgo.Description("Feature fields; must include geojson and name")), ), func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) { mapID := req.GetString("map_id", "") @@ -474,8 +498,8 @@ func addNestedCRUD(s *server.MCPServer, sess *Session, r nestedMapResource) { mcpgo.NewTool(r.listTool, mcpgo.WithDescription(fmt.Sprintf("List %ss for a map.", r.humanName)), r.parentArg, - mcpgo.WithNumber("page", mcpgo.Description("Page number")), - mcpgo.WithNumber("per_page", mcpgo.Description("Items per page")), + pageParam(), + perPageParam(), ), func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) { mapID := req.GetString("map_id", "") @@ -694,8 +718,8 @@ func addLinkableCRUD(s *server.MCPServer, sess *Session, r linkableMapResource) s.AddTool( mcpgo.NewTool(r.listTool, mcpgo.WithDescription(fmt.Sprintf("List %ss. Pass at most one of collection_id or story_section_id to scope.", r.humanName)), - mcpgo.WithNumber("page", mcpgo.Description("Page number")), - mcpgo.WithNumber("per_page", mcpgo.Description("Items per page")), + pageParam(), + perPageParam(), mcpgo.WithString("collection_id", mcpgo.Description("Filter by parent collection")), mcpgo.WithString("story_section_id", mcpgo.Description("Filter by parent story section")), ), diff --git a/internal/mcp/tools_media.go b/internal/mcp/tools_media.go index 4d90ef0..ce6571a 100644 --- a/internal/mcp/tools_media.go +++ b/internal/mcp/tools_media.go @@ -4,21 +4,13 @@ import ( "context" "fmt" "path/filepath" + "strings" mcpgo "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "github.com/mytours/stqry-cli/internal/api" ) -// validMediaTypes is a fast-lookup set built from api.ValidMediaTypes. -var validMediaTypes = func() map[string]bool { - m := make(map[string]bool, len(api.ValidMediaTypes)) - for _, t := range api.ValidMediaTypes { - m[t] = true - } - return m -}() - func registerMediaTools(s *server.MCPServer, sess *Session) { // create_media: uploads a file and creates a new media item s.AddTool( @@ -30,10 +22,11 @@ func registerMediaTools(s *server.MCPServer, sess *Session) { ), mcpgo.WithString("type", mcpgo.Required(), - mcpgo.Description("Media item type: map, webpackage, animation, audio, image, video, webvideo, ar, data"), + mcpgo.Description("Media item type: "+strings.Join(api.ValidMediaTypes, ", ")), ), mcpgo.WithString("name", - mcpgo.Description("Name for the media item"), + mcpgo.Required(), + mcpgo.Description("Name for the media item (internal label). Required — the server rejects a media item with a blank name (422 \"Name can't be blank\") for every media type."), ), ), func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) { @@ -48,11 +41,8 @@ func registerMediaTools(s *server.MCPServer, sess *Session) { if mediaType == "" { return mcpgo.NewToolResultError("type is required"), nil } - if !validMediaTypes[mediaType] { - return mcpgo.NewToolResultError(fmt.Sprintf( - "invalid type %q: must be one of map, webpackage, animation, audio, image, video, webvideo, ar, data", - mediaType, - )), nil + if err := api.ValidateMediaType(mediaType); err != nil { + return mcpgo.NewToolResultError(err.Error()), nil } // Mirror the CLI's client-side guard: `webvideo` requires a Remote // Subtype payload (account, remote_type) the CLI/MCP cannot author @@ -61,6 +51,13 @@ func registerMediaTools(s *server.MCPServer, sess *Session) { return mcpgo.NewToolResultError("type \"webvideo\" requires a Remote subtype (account, remote_type) that the MCP cannot author yet. For a direct external-stream URL use type \"video\" with the URL passed via the fields map; for an embedded YouTube/Vimeo video build it in STQRY Builder"), nil } name := req.GetString("name", "") + if name == "" { + // MediaItem validates name presence for every subtype and the + // public API doesn't derive one from the filename, so a blank + // name is a guaranteed 422. Fail fast — and before the upload, + // so we don't leave an orphaned uploaded file behind. + return mcpgo.NewToolResultError("name is required (the server rejects a media item with a blank name)"), nil + } client, err := ResolveClient(sess) if err != nil { @@ -102,12 +99,8 @@ func registerMediaTools(s *server.MCPServer, sess *Session) { s.AddTool( mcpgo.NewTool("list_media", mcpgo.WithDescription("List all media items for the configured STQRY account."), - mcpgo.WithNumber("page", - mcpgo.Description("Page number (1-based, default: 1)"), - ), - mcpgo.WithNumber("per_page", - mcpgo.Description("Items per page (default: 25)"), - ), + pageParam(), + perPageParam(), ), func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) { client, err := ResolveClient(sess) @@ -189,22 +182,29 @@ func registerMediaTools(s *server.MCPServer, sess *Session) { // delete_media: deletes a media item by ID s.AddTool( mcpgo.NewTool("delete_media", - mcpgo.WithDescription("Delete a STQRY media item by ID."), + mcpgo.WithDescription("Delete a STQRY media item by ID. Pass `language` to drop only that locale's translations instead of the whole media item."), mcpgo.WithString("id", mcpgo.Required(), mcpgo.Description("The media item ID"), ), + mcpgo.WithString("language", + mcpgo.Description("Optional locale (e.g. 'fr') — drops only that translation; the media item itself stays."), + ), ), func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) { id := req.GetString("id", "") if id == "" { return mcpgo.NewToolResultError("id is required"), nil } + var query map[string]string + if lang := req.GetString("language", ""); lang != "" { + query = map[string]string{"language": lang} + } client, err := ResolveClient(sess) if err != nil { return mcpgo.NewToolResultError(fmt.Sprintf("resolving client: %v", err)), nil } - if err := api.DeleteMediaItem(client, id, nil); err != nil { + if err := api.DeleteMediaItem(client, id, query); err != nil { return mcpgo.NewToolResultError(fmt.Sprintf("deleting media item: %v", err)), nil } return mcpgo.NewToolResultText(`{"ok":true}`), nil diff --git a/internal/mcp/tools_project_tabs.go b/internal/mcp/tools_project_tabs.go index 54ae99f..1d39b2f 100644 --- a/internal/mcp/tools_project_tabs.go +++ b/internal/mcp/tools_project_tabs.go @@ -17,8 +17,8 @@ func registerProjectTabTools(s *server.MCPServer, sess *Session) { mcpgo.NewTool("list_project_tabs", mcpgo.WithDescription("List the bottom-nav tabs on a STQRY project."), mcpgo.WithString("project_id", mcpgo.Required(), mcpgo.Description("The project ID")), - mcpgo.WithNumber("page", mcpgo.Description("Page number (1-based, default: 1)")), - mcpgo.WithNumber("per_page", mcpgo.Description("Items per page (max 1000, default: 30)")), + pageParam(), + perPageParam(), ), func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) { projectID := req.GetString("project_id", "") diff --git a/internal/mcp/tools_projects.go b/internal/mcp/tools_projects.go index 5e314a8..737a630 100644 --- a/internal/mcp/tools_projects.go +++ b/internal/mcp/tools_projects.go @@ -14,12 +14,8 @@ func registerProjectTools(s *server.MCPServer, sess *Session) { s.AddTool( mcpgo.NewTool("list_projects", mcpgo.WithDescription("List all projects for the configured STQRY account."), - mcpgo.WithNumber("page", - mcpgo.Description("Page number (1-based, default: 1)"), - ), - mcpgo.WithNumber("per_page", - mcpgo.Description("Items per page (default: 25)"), - ), + pageParam(), + perPageParam(), ), func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) { client, err := ResolveClient(sess) @@ -65,10 +61,10 @@ func registerProjectTools(s *server.MCPServer, sess *Session) { s.AddTool( mcpgo.NewTool("create_project", - mcpgo.WithDescription("Create a new STQRY project. Public API supports projectable_type values Project::App, Project::Kiosk, Project::Signage."), + mcpgo.WithDescription("Create a new STQRY project. Required: name and projectable_type (one of: Project::App, Project::Kiosk, Project::Signage)."), mcpgo.WithObject("fields", mcpgo.Required(), - mcpgo.Description("Arbitrary JSON object of project fields. Required: name, projectable_type, primary_language_code."), + mcpgo.Description("Project fields. Required: name, projectable_type. Optional: primary_language_code (defaults server-side — NOT required, unlike collections/screens), app_name, subdomain, additional_language_codes, tags, and the published_on_* / show_in_app_catalogue booleans. Example: {\"name\": \"My Tour App\", \"projectable_type\": \"Project::App\"}."), ), ), func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) { diff --git a/internal/mcp/tools_screens.go b/internal/mcp/tools_screens.go index d905c4b..0bcd8e3 100644 --- a/internal/mcp/tools_screens.go +++ b/internal/mcp/tools_screens.go @@ -65,12 +65,8 @@ func registerScreenCRUD(s *server.MCPServer, sess *Session) { s.AddTool( mcpgo.NewTool("list_screens", mcpgo.WithDescription("List all screens for the configured STQRY account."), - mcpgo.WithNumber("page", - mcpgo.Description("Page number (1-based, default: 1)"), - ), - mcpgo.WithNumber("per_page", - mcpgo.Description("Items per page (default: 25)"), - ), + pageParam(), + perPageParam(), ), func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) { client, err := ResolveClient(sess) @@ -117,10 +113,10 @@ func registerScreenCRUD(s *server.MCPServer, sess *Session) { // create_screen: creates a new screen with the given fields s.AddTool( mcpgo.NewTool("create_screen", - mcpgo.WithDescription("Create a new STQRY screen."), + mcpgo.WithDescription("Create a new STQRY screen. Required: name, type (one of: story, web, panorama, ar, kiosk), and primary_language (e.g. \"en\"). The server also requires title and short_title on the primary language; pass translated fields as locale maps, e.g. {\"en\": \"…\"}."), mcpgo.WithObject("fields", mcpgo.Required(), - mcpgo.Description("Arbitrary JSON object of screen fields to set"), + mcpgo.Description("Screen fields. Required: name, type, primary_language, plus title and short_title on the primary language. Example: {\"name\": \"Welcome\", \"type\": \"story\", \"primary_language\": \"en\", \"title\": {\"en\": \"Welcome\"}, \"short_title\": {\"en\": \"Hi\"}}. Optional: header_layout (none/image/image_and_title/short/tall), the *_media_item_id covers/background/logo."), ), ), func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) { @@ -144,7 +140,7 @@ func registerScreenCRUD(s *server.MCPServer, sess *Session) { // update_screen: updates an existing screen by ID s.AddTool( mcpgo.NewTool("update_screen", - mcpgo.WithDescription("Update an existing STQRY screen by ID."), + mcpgo.WithDescription("Update an existing STQRY screen by ID. Translated fields (title, short_title) are locale maps, e.g. {\"en\": \"…\"}. Pass primary_language to flip the primary locale — the target locale must already have title + short_title populated, or the server rejects the flip."), mcpgo.WithString("id", mcpgo.Required(), mcpgo.Description("The screen ID"), @@ -218,12 +214,8 @@ func registerSectionCRUD(s *server.MCPServer, sess *Session) { mcpgo.Required(), mcpgo.Description("The screen ID"), ), - mcpgo.WithNumber("page", - mcpgo.Description("Page number (1-based, default: 1)"), - ), - mcpgo.WithNumber("per_page", - mcpgo.Description("Items per page (default: 25)"), - ), + pageParam(), + perPageParam(), ), func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) { screenID := req.GetString("screen_id", "") @@ -282,14 +274,14 @@ func registerSectionCRUD(s *server.MCPServer, sess *Session) { // create_section: creates a new story section for a screen s.AddTool( mcpgo.NewTool("create_section", - mcpgo.WithDescription("Create a new STQRY story section for a screen."), + mcpgo.WithDescription("Create a new STQRY story section on a screen. Required: type — one of text, single_media, media_group, image_slider, link_group, social_group, location, menu, opening_time_group, price_group, badge_group, form, custom_widget. (quiz_question / quiz_score are real section types but require a Subtype payload this tool cannot author yet — build those in STQRY Builder.)"), mcpgo.WithString("screen_id", mcpgo.Required(), mcpgo.Description("The screen ID"), ), mcpgo.WithObject("fields", mcpgo.Required(), - mcpgo.Description("Arbitrary JSON object of section fields to set"), + mcpgo.Description("Section fields. Required: type (see the tool description for the creatable set). Translated fields (title, body, …) are locale maps. Example: {\"type\": \"text\", \"title\": {\"en\": \"About\"}, \"body\": {\"en\": \"
Hello
\"}}."), ), ), func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) { diff --git a/internal/mcp/tools_uploaded_files.go b/internal/mcp/tools_uploaded_files.go index bc0f17a..c0e1445 100644 --- a/internal/mcp/tools_uploaded_files.go +++ b/internal/mcp/tools_uploaded_files.go @@ -13,12 +13,8 @@ func registerUploadedFileTools(s *server.MCPServer, sess *Session) { s.AddTool( mcpgo.NewTool("list_uploaded_files", mcpgo.WithDescription("List uploaded_file metadata records (the binaries that media items reference)."), - mcpgo.WithNumber("page", - mcpgo.Description("Page number (1-based, default: 1)"), - ), - mcpgo.WithNumber("per_page", - mcpgo.Description("Items per page (default: 30, max: 1000)"), - ), + pageParam(), + perPageParam(), ), func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) { client, err := ResolveClient(sess)