diff --git a/Tests/blitz_tests/MCPRegistryCompatibilityTests.swift b/Tests/blitz_tests/MCPRegistryCompatibilityTests.swift index d49be13..c652bd9 100644 --- a/Tests/blitz_tests/MCPRegistryCompatibilityTests.swift +++ b/Tests/blitz_tests/MCPRegistryCompatibilityTests.swift @@ -32,6 +32,15 @@ struct MCPRegistryCompatibilityTests { #expect(properties["bundleId"] != nil) } + @Test func ascCreateSubscriptionSupportsExistingGroupId() throws { + let tools = MCPRegistry.allTools() + let createSubscription = try #require(tool(named: "asc_create_subscription", in: tools)) + let schema = try #require(createSubscription["inputSchema"] as? [String: Any]) + let properties = try #require(schema["properties"] as? [String: Any]) + #expect(properties["groupId"] != nil) + #expect((schema["required"] as? [String])?.contains("groupName") == false) + } + @Test func screenshotRegistryUsesDirectTrackTools() { let tools = MCPRegistry.allTools() #expect(tool(named: "screenshots_add_asset", in: tools) == nil) diff --git a/docs/iap-subscription-pricing-plan.md b/docs/iap-subscription-pricing-plan.md index ef5e574..5f8dd21 100644 --- a/docs/iap-subscription-pricing-plan.md +++ b/docs/iap-subscription-pricing-plan.md @@ -57,7 +57,7 @@ This avoids changing any existing callers. | Tool | Required Params | Key Enums | |------|----------------|-----------| | `asc_create_iap` | productId, name, type, displayName, price | type: CONSUMABLE, NON_CONSUMABLE, NON_RENEWING_SUBSCRIPTION | -| `asc_create_subscription` | groupName, productId, name, displayName, duration, price | duration: ONE_WEEK..ONE_YEAR | +| `asc_create_subscription` | groupId or groupName, productId, name, displayName, duration, price | `groupId` adds to an existing group; duration: ONE_WEEK..ONE_YEAR | | `asc_set_app_price` | price | — | **B) Add category mapping** (in `category(for:)` at line ~469): diff --git a/src/managers/asc/ASCMonetizationManager.swift b/src/managers/asc/ASCMonetizationManager.swift index c60ab36..2655a01 100644 --- a/src/managers/asc/ASCMonetizationManager.swift +++ b/src/managers/asc/ASCMonetizationManager.swift @@ -198,9 +198,13 @@ extension ASCManager { // MARK: - Subscription Creation - func createSubscription(groupName: String, name: String, productId: String, displayName: String, description: String?, duration: String, price: String, screenshotPath: String? = nil) { + func createSubscription(groupName: String? = nil, groupId: String? = nil, name: String, productId: String, displayName: String, description: String?, duration: String, price: String, screenshotPath: String? = nil) { guard let service else { return } guard let appId = app?.id else { return } + guard groupId != nil || groupName != nil else { + writeError = "A subscription group ID or name is required." + return + } let startedAt = Date() writeError = nil isCreating = true @@ -212,15 +216,26 @@ extension ASCManager { do { createProgress = 0.03 let group: ASCSubscriptionGroup - if let existing = subscriptionGroups.first(where: { $0.attributes.referenceName == groupName }) { + let groups = try await service.fetchSubscriptionGroups(appId: appId) + if let groupId, + let existing = groups.first(where: { $0.id == groupId }) { + group = existing + } else if let groupName, + let existing = groups.first(where: { $0.attributes.referenceName == groupName }) { let groupLocs = try await service.fetchSubscriptionGroupLocalizations(groupId: existing.id) if groupLocs.isEmpty { try await service.localizeSubscriptionGroup(groupId: existing.id, locale: "en-US", name: groupName) } group = existing - } else { + } else if let groupName { group = try await service.createSubscriptionGroup(appId: appId, referenceName: groupName) try await service.localizeSubscriptionGroup(groupId: group.id, locale: "en-US", name: groupName) + } else { + throw NSError( + domain: "ASCMonetization", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "Subscription group \(groupId ?? "") was not found."] + ) } createProgressMessage = "Creating subscription…" diff --git a/src/resources/CLAUDE.md.template b/src/resources/CLAUDE.md.template index d71c30a..f1889ed 100644 --- a/src/resources/CLAUDE.md.template +++ b/src/resources/CLAUDE.md.template @@ -166,11 +166,12 @@ Create an in-app purchase. Creates the IAP, adds en-US localization, and sets th ### asc_create_subscription -Create an auto-renewable subscription. Creates or reuses a subscription group. +Create an auto-renewable subscription. Pass `groupId` to add it to an existing subscription group, or `groupName` to create/reuse a group by reference name. | param | type | required | notes | |---|---|---|---| -| groupName | string | yes | Subscription group name (created if doesn't exist) | +| groupId | string | one of groupId/groupName | Existing App Store Connect subscription group ID; preferred when adding to an existing group | +| groupName | string | one of groupId/groupName | Subscription group reference name (created if it doesn't exist) | | productId | string | yes | Unique product identifier | | name | string | yes | Internal reference name | | displayName | string | yes | User-facing display name (en-US) | diff --git a/src/services/mcp/MCPExecutorASC.swift b/src/services/mcp/MCPExecutorASC.swift index f2cc4cb..2d59d6d 100644 --- a/src/services/mcp/MCPExecutorASC.swift +++ b/src/services/mcp/MCPExecutorASC.swift @@ -1442,8 +1442,7 @@ extension MCPExecutor { } func executeASCCreateSubscription(_ args: [String: Any]) async throws -> [String: Any] { - guard let groupName = args["groupName"] as? String, - let productId = args["productId"] as? String, + guard let productId = args["productId"] as? String, let name = args["name"] as? String, let displayName = args["displayName"] as? String, let duration = args["duration"] as? String, @@ -1451,6 +1450,11 @@ extension MCPExecutor { let screenshotPath = args["screenshotPath"] as? String else { throw MCPServerService.MCPError.invalidToolArgs } + let groupId = args["groupId"] as? String + let groupName = args["groupName"] as? String + guard (groupId?.isEmpty == false) || (groupName?.isEmpty == false) else { + return mcpText("Error: provide either groupId for an existing subscription group or groupName for a new/reused group.") + } let description = args["description"] as? String let validDurations = ["ONE_WEEK", "ONE_MONTH", "TWO_MONTHS", "THREE_MONTHS", "SIX_MONTHS", "ONE_YEAR"] @@ -1463,13 +1467,14 @@ extension MCPExecutor { await MainActor.run { var values: [String: String] = [ "kind": "subscription", - "groupName": groupName, "name": name, "productId": productId, "displayName": displayName, "duration": duration, "price": priceStr ] + if let groupId, !groupId.isEmpty { values["groupId"] = groupId } + if let groupName, !groupName.isEmpty { values["groupName"] = groupName } if let description { values["description"] = description } appState.ascManager.pendingCreateValues = values } @@ -1477,6 +1482,7 @@ extension MCPExecutor { await MainActor.run { appState.ascManager.createSubscription( groupName: groupName, + groupId: groupId, name: name, productId: productId, displayName: displayName, @@ -1491,14 +1497,16 @@ extension MCPExecutor { return mcpText("Error creating subscription: \(error)") } - return mcpJSON([ + var result: [String: Any] = [ "success": true, - "groupName": groupName, "productId": productId, "displayName": displayName, "duration": duration, "price": priceStr - ]) + ] + if let groupId, !groupId.isEmpty { result["groupId"] = groupId } + if let groupName, !groupName.isEmpty { result["groupName"] = groupName } + return mcpJSON(result) } func pollASCCreation() async -> String? { diff --git a/src/services/mcp/MCPRegistry.swift b/src/services/mcp/MCPRegistry.swift index 467c801..9cf0246 100644 --- a/src/services/mcp/MCPRegistry.swift +++ b/src/services/mcp/MCPRegistry.swift @@ -326,9 +326,10 @@ enum MCPRegistry { tools.append(tool( name: "asc_create_subscription", - description: "Create an auto-renewable subscription in App Store Connect. Creates or reuses a subscription group, adds the subscription with en-US localization, sets the price, and uploads a review screenshot. You MUST provide a screenshot — take one of the subscription content in the app simulator first (e.g. the paywall or premium features screen), save to /tmp, then pass the path.", + description: "Create an auto-renewable subscription in App Store Connect. Provide groupId to add it to an existing subscription group, or groupName to create/reuse a group by reference name. Adds en-US localization, sets the price, and uploads a review screenshot. You MUST provide a screenshot — take one of the subscription content in the app simulator first (e.g. the paywall or premium features screen), save to /tmp, then pass the path.", properties: [ - "groupName": ["type": "string", "description": "Subscription group name (created if doesn't exist)"], + "groupId": ["type": "string", "description": "Existing App Store Connect subscription group ID. Use this to add a product to an existing group."], + "groupName": ["type": "string", "description": "Subscription group reference name. Used only when groupId is omitted; creates the group if it does not exist."], "productId": ["type": "string", "description": "Unique product identifier"], "name": ["type": "string", "description": "Internal reference name"], "displayName": ["type": "string", "description": "User-facing display name (en-US localization)"], @@ -339,7 +340,7 @@ enum MCPRegistry { "description": ["type": "string", "description": "User-facing description (optional)"], "screenshotPath": ["type": "string", "description": "Path to review screenshot (min 640x920px PNG/JPEG). Use blitz-iphone to take a screenshot of the subscription content in the simulator."] ], - required: ["groupName", "productId", "name", "displayName", "duration", "price", "screenshotPath"] + required: ["productId", "name", "displayName", "duration", "price", "screenshotPath"] )) tools.append(tool(