Skip to content

Commit 6e7d898

Browse files
zelentsov-devclaude
andcommitted
feat: add screenshots_upload_batch for multi-file upload + core API audit (293 tools)
New tool: - screenshots_upload_batch: upload multiple screenshots to a set in one call Accepts set_id + file_paths array, uploads each sequentially (reserve→upload→commit) Returns per-file results with success/failure status Core API audit: confirmed 100% coverage of everyday developer workflows - App management, version localizations, categories, pricing - TestFlight, subscriptions, IAP, reviews, provisioning - No critical gaps found in Tier 1/2 functionality Total: 33 workers, 293 tools, 436 tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2fae446 commit 6e7d898

7 files changed

Lines changed: 140 additions & 9 deletions

File tree

CLAUDE.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ MCP (Model Context Protocol) server for App Store Connect API integration, desig
1616
# Build the project
1717
swift build
1818

19-
# Run all unit tests (435 tests)
19+
# Run all unit tests (436 tests)
2020
swift test
2121

2222
# Run the MCP server (requires environment variables or companies.json)
@@ -49,7 +49,7 @@ Each company needs: `keyID`, `issuerID`, `privateKeyPath` (path to `.p8` file).
4949

5050
**WorkerManager** (`Workers/MainWorker/WorkerManager.swift`) — central registry, routes tool calls by prefix.
5151

52-
**Workers** (33 workers, 292 tools):
52+
**Workers** (33 workers, 293 tools):
5353

5454
| Worker | Prefix | Tools | Domain |
5555
|--------|--------|-------|--------|
@@ -79,7 +79,7 @@ Each company needs: `keyID`, `issuerID`, `privateKeyPath` (path to `.p8` file).
7979
| BetaAppWorker | `beta_app_` | 10 | Beta app localizations, review submissions, review details |
8080
| PreReleaseVersionsWorker | `pre_release_` | 3 | Pre-release versions (list, get, builds) |
8181
| BetaLicenseAgreementsWorker | `beta_license_` | 3 | Beta license agreements (list, get, update) |
82-
| ScreenshotsWorker | `screenshots_` | 15 | Screenshots, previews, sets, reorder, full upload |
82+
| ScreenshotsWorker | `screenshots_` | 16 | Screenshots, previews, sets, reorder, full upload, batch upload |
8383
| CustomProductPagesWorker | `custom_pages_` | 10 | Custom product pages, versions, localizations |
8484
| ProductPageOptimizationWorker | `ppo_` | 9 | A/B test experiments, treatments |
8585
| PromotedPurchasesWorker | `promoted_` | 9 | Promoted in-app purchases, images upload |
@@ -110,7 +110,7 @@ Each company needs: `keyID`, `issuerID`, `privateKeyPath` (path to `.p8` file).
110110
### Unit Tests (Swift Testing)
111111

112112
```bash
113-
swift test # Run all 435 tests across 31 suites
113+
swift test # Run all 436 tests across 31 suites
114114
```
115115

116116
Test categories:

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
## Overview
3131

32-
**asc-mcp** is a Swift-based MCP server that bridges [Claude](https://claude.ai) (or any MCP-compatible host) with the [App Store Connect API](https://developer.apple.com/documentation/appstoreconnectapi). It exposes **292 tools** across 33 workers, enabling you to automate your entire iOS/macOS release workflow through natural language.
32+
**asc-mcp** is a Swift-based MCP server that bridges [Claude](https://claude.ai) (or any MCP-compatible host) with the [App Store Connect API](https://developer.apple.com/documentation/appstoreconnectapi). It exposes **293 tools** across 33 workers, enabling you to automate your entire iOS/macOS release workflow through natural language.
3333

3434
### Key capabilities
3535

@@ -368,7 +368,7 @@ Add to `~/.codeium/windsurf/mcp_config.json`:
368368
369369
### Worker Filtering
370370
371-
The server exposes **292 tools** across 33 workers. Some MCP clients impose a tool limit (e.g., Windsurf caps at 100). Use `--workers` to enable only the workers you need:
371+
The server exposes **293 tools** across 33 workers. Some MCP clients impose a tool limit (e.g., Windsurf caps at 100). Use `--workers` to enable only the workers you need:
372372
373373
```bash
374374
# Only load apps, builds, and version lifecycle tools
@@ -432,7 +432,7 @@ For Claude (200K context) ~22K tokens is ~5–7% — negligible. For clients wit
432432
433433
## Available Tools
434434
435-
**292 tools** organized across 33 workers (use `--workers` to filter — see [Worker Filtering](#worker-filtering)):
435+
**293 tools** organized across 33 workers (use `--workers` to filter — see [Worker Filtering](#worker-filtering)):
436436
437437
<details>
438438
<summary><strong>Company Management</strong> — 3 tools</summary>

Sources/asc-mcp/Workers/ScreenshotsWorker/ScreenshotsWorker+Handlers.swift

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,100 @@ extension ScreenshotsWorker {
273273
}
274274
}
275275

276+
/// Uploads multiple screenshots to a set sequentially
277+
/// - Returns: JSON array with results for each file (success or error)
278+
func uploadScreenshotBatch(_ params: CallTool.Parameters) async throws -> CallTool.Result {
279+
guard let arguments = params.arguments,
280+
let setId = arguments["set_id"]?.stringValue,
281+
let filePaths = arguments["file_paths"]?.arrayValue?.compactMap({ $0.stringValue }),
282+
!filePaths.isEmpty else {
283+
return CallTool.Result(
284+
content: [.text("Error: Required parameters: set_id, file_paths (non-empty array)")],
285+
isError: true
286+
)
287+
}
288+
289+
var results: [[String: Any]] = []
290+
var successCount = 0
291+
var failCount = 0
292+
293+
for filePath in filePaths {
294+
do {
295+
// Step 1: Get file info
296+
let fileSize = try await uploadService.fileSize(at: filePath)
297+
let fileName = await uploadService.fileName(at: filePath)
298+
299+
// Step 2: Reserve
300+
let createRequest = CreateScreenshotRequest(
301+
data: CreateScreenshotRequest.CreateData(
302+
attributes: CreateScreenshotRequest.Attributes(
303+
fileName: fileName,
304+
fileSize: fileSize
305+
),
306+
relationships: CreateScreenshotRequest.Relationships(
307+
appScreenshotSet: CreateScreenshotRequest.ScreenshotSetRelationship(
308+
data: ASCResourceIdentifier(type: "appScreenshotSets", id: setId)
309+
)
310+
)
311+
)
312+
)
313+
314+
let encoder = JSONEncoder()
315+
let bodyData = try encoder.encode(createRequest)
316+
let reserveData = try await httpClient.post("/v1/appScreenshots", body: bodyData)
317+
let reserveResponse = try JSONDecoder().decode(ASCScreenshotResponse.self, from: reserveData)
318+
319+
let screenshotId = reserveResponse.data.id
320+
guard let uploadOperations = reserveResponse.data.attributes?.uploadOperations, !uploadOperations.isEmpty else {
321+
results.append(["file": fileName, "success": false, "error": "No upload operations returned"])
322+
failCount += 1
323+
continue
324+
}
325+
326+
// Step 3: Upload chunks
327+
let md5 = try await uploadService.uploadFile(filePath: filePath, uploadOperations: uploadOperations)
328+
329+
// Step 4: Commit
330+
let commitRequest = CommitScreenshotRequest(
331+
data: CommitScreenshotRequest.CommitData(
332+
id: screenshotId,
333+
attributes: CommitScreenshotRequest.Attributes(
334+
sourceFileChecksum: md5,
335+
uploaded: true
336+
)
337+
)
338+
)
339+
340+
let commitBody = try encoder.encode(commitRequest)
341+
let commitData = try await httpClient.patch("/v1/appScreenshots/\(screenshotId)", body: commitBody)
342+
let commitResponse = try JSONDecoder().decode(ASCScreenshotResponse.self, from: commitData)
343+
344+
results.append([
345+
"file": fileName,
346+
"success": true,
347+
"screenshot_id": commitResponse.data.id,
348+
"state": commitResponse.data.attributes?.assetDeliveryState?.state ?? "unknown"
349+
] as [String: Any])
350+
successCount += 1
351+
352+
} catch {
353+
let fileName = URL(fileURLWithPath: filePath).lastPathComponent
354+
results.append(["file": fileName, "success": false, "error": error.localizedDescription] as [String: Any])
355+
failCount += 1
356+
}
357+
}
358+
359+
let response: [String: Any] = [
360+
"success": failCount == 0,
361+
"total": filePaths.count,
362+
"uploaded": successCount,
363+
"failed": failCount,
364+
"results": results
365+
]
366+
367+
return CallTool.Result(content: [.text(JSONFormatter.formatJSON(response))])
368+
}
369+
276370
/// Gets details of a specific screenshot
277371
/// - Returns: JSON with screenshot details
278372
func getScreenshot(_ params: CallTool.Parameters) async throws -> CallTool.Result {

Sources/asc-mcp/Workers/ScreenshotsWorker/ScreenshotsWorker+ToolDefinitions.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,30 @@ extension ScreenshotsWorker {
298298
)
299299
}
300300

301+
func uploadScreenshotBatchTool() -> Tool {
302+
return Tool(
303+
name: "screenshots_upload_batch",
304+
description: "Upload multiple screenshots to a screenshot set in one call. Each file goes through the full upload cycle (reserve, upload, commit). Returns results for each file.",
305+
inputSchema: .object([
306+
"type": .string("object"),
307+
"properties": .object([
308+
"set_id": .object([
309+
"type": .string("string"),
310+
"description": .string("Screenshot set ID")
311+
]),
312+
"file_paths": .object([
313+
"type": .string("array"),
314+
"description": .string("Array of absolute paths to screenshot files on disk"),
315+
"items": .object([
316+
"type": .string("string")
317+
])
318+
])
319+
]),
320+
"required": .array([.string("set_id"), .string("file_paths")])
321+
])
322+
)
323+
}
324+
301325
func deletePreviewTool() -> Tool {
302326
return Tool(
303327
name: "screenshots_delete_preview",

Sources/asc-mcp/Workers/ScreenshotsWorker/ScreenshotsWorker.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ public final class ScreenshotsWorker: Sendable {
2828
uploadPreviewTool(),
2929
getPreviewTool(),
3030
listPreviewsTool(),
31-
deletePreviewTool()
31+
deletePreviewTool(),
32+
uploadScreenshotBatchTool()
3233
]
3334
}
3435

@@ -65,6 +66,8 @@ public final class ScreenshotsWorker: Sendable {
6566
return try await listPreviews(params)
6667
case "screenshots_delete_preview":
6768
return try await deletePreview(params)
69+
case "screenshots_upload_batch":
70+
return try await uploadScreenshotBatch(params)
6871
default:
6972
throw MCPError.methodNotFound("Unknown tool: \(params.name)")
7073
}

Tests/ASCMCPTests/Workers/ParameterValidationTests.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,15 @@ struct ParameterValidationTests {
935935
#expect(result.isError == true)
936936
}
937937

938+
@Test("screenshots_upload_batch without required params returns isError")
939+
func screenshotsUploadBatchMissing() async throws {
940+
let client = try await TestFactory.makeHTTPClient()
941+
let worker = ScreenshotsWorker(httpClient: client, uploadService: UploadService())
942+
let params = CallTool.Parameters(name: "screenshots_upload_batch", arguments: nil)
943+
let result = try await worker.handleTool(params)
944+
#expect(result.isError == true)
945+
}
946+
938947
@Test("screenshots_delete without screenshot_id returns isError")
939948
func screenshotsDeleteMissing() async throws {
940949
let client = try await TestFactory.makeHTTPClient()

Tests/ASCMCPTests/Workers/WorkerToolDefinitionsTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ struct WorkerToolDefinitionsTests {
483483
let client = try await TestFactory.makeHTTPClient()
484484
let worker = ScreenshotsWorker(httpClient: client, uploadService: UploadService())
485485
let tools = await worker.getTools()
486-
#expect(tools.count == 15)
486+
#expect(tools.count == 16)
487487
let names = Set(tools.map(\.name))
488488
#expect(names.contains("screenshots_list_sets"))
489489
#expect(names.contains("screenshots_create_set"))
@@ -500,6 +500,7 @@ struct WorkerToolDefinitionsTests {
500500
#expect(names.contains("screenshots_get_preview"))
501501
#expect(names.contains("screenshots_list_previews"))
502502
#expect(names.contains("screenshots_delete_preview"))
503+
#expect(names.contains("screenshots_upload_batch"))
503504
}
504505

505506
// MARK: - CustomProductPagesWorker (10 tools)

0 commit comments

Comments
 (0)