Skip to content

Commit 6451482

Browse files
committed
Add workflow details tool
1 parent 3f8cf3d commit 6451482

7 files changed

Lines changed: 388 additions & 6 deletions

File tree

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Minimal MCP server for discovering Xcode Cloud products and workflows, then retr
1414

1515
- Discover Xcode Cloud products with `list_products`.
1616
- Discover workflows for a product with `list_workflows`.
17+
- Retrieve workflow configuration with `get_workflow_details`.
1718
- List recent workflow runs with `list_build_runs`.
1819
- Retrieve build issue counts with `get_build_issues`.
1920
- Retrieve and summarize text-like build logs with `get_build_logs`.
@@ -71,6 +72,7 @@ codex mcp add xcode-cloud \
7172

7273
- `list_products(limit?)`
7374
- `list_workflows(productId, limit?)`
75+
- `get_workflow_details(workflowId)`
7476
- `list_build_runs(workflowId, limit?, status?)`
7577
- `get_build_issues(buildRunId? workflowId? buildNumber? buildSelector?)`
7678
- `get_build_logs(buildRunId? workflowId? buildNumber? buildSelector?, maxCharacters?)`
@@ -136,6 +138,26 @@ Show me the latest failing UI test artifacts for workflow abc123.
136138
List the workflows for product def456 and then summarize the latest build.
137139
```
138140

141+
```text
142+
Show me the full workflow details for workflow abc123, including environment, start conditions, actions, and whether it is enabled.
143+
```
144+
145+
## Workflow Details Behavior
146+
147+
`get_workflow_details` returns the live workflow configuration exposed by App Store Connect, grouped into:
148+
149+
- `general`
150+
- `environment`
151+
- `startConditions`
152+
- `actions`
153+
- `postActions`
154+
155+
Notes:
156+
157+
- `environment` includes repository, `xcodeVersion`, and `macOsVersion` when App Store Connect returns them.
158+
- `actions` includes action type, scheme, platform, destination, required-to-pass state, and test-plan details when present.
159+
- `postActions` is currently returned as an empty array with a note because the App Store Connect workflow payload does not expose separate post-actions in the observed API response.
160+
139161
## Local Development
140162

141163
Install dependencies:

src/api/base-client.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,15 @@ export class BaseAPIClient {
1818
protected async get<TData>(
1919
path: string,
2020
params?: Record<string, string>,
21-
): Promise<APIResponse<TData>> {
21+
): Promise<APIResponse<TData>>;
22+
protected async get<TData, TIncluded>(
23+
path: string,
24+
params?: Record<string, string>,
25+
): Promise<APIResponse<TData, TIncluded>>;
26+
protected async get<TData, TIncluded>(
27+
path: string,
28+
params?: Record<string, string>,
29+
): Promise<APIResponse<TData, TIncluded>> {
2230
const url = new URL(path, this.baseUrl);
2331

2432
for (const [key, value] of Object.entries(params ?? {})) {
@@ -44,7 +52,9 @@ export class BaseAPIClient {
4452
return new Uint8Array(await response.arrayBuffer());
4553
}
4654

47-
private async request<TData>(url: string): Promise<APIResponse<TData>> {
55+
private async request<TData, TIncluded = never>(
56+
url: string,
57+
): Promise<APIResponse<TData, TIncluded>> {
4858
const response = await fetch(url, {
4959
headers: {
5060
Authorization: `Bearer ${this.auth.getToken()}`,
@@ -60,7 +70,8 @@ export class BaseAPIClient {
6070
);
6171
}
6272

63-
const payload = (await response.json()) as APIResponse<TData> | APIErrorResponse;
73+
const payload =
74+
(await response.json()) as APIResponse<TData, TIncluded> | APIErrorResponse;
6475

6576
if (!response.ok) {
6677
const apiErrorResponse = payload as APIErrorResponse;
@@ -71,6 +82,6 @@ export class BaseAPIClient {
7182
throw new Error(`API Error (${response.status}): ${message}`);
7283
}
7384

74-
return payload as APIResponse<TData>;
85+
return payload as APIResponse<TData, TIncluded>;
7586
}
7687
}

src/api/resources/workflows.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { BaseAPIClient } from '../base-client.js';
2-
import type { CiWorkflow } from '../types.js';
2+
import type { CiWorkflow, WorkflowIncludedResource } from '../types.js';
33

44
/**
55
* Workflow endpoints.
@@ -18,4 +18,26 @@ export class WorkflowsClient extends BaseAPIClient {
1818

1919
return response.data;
2020
}
21+
22+
/**
23+
* Fetch one workflow with repository and environment details.
24+
*/
25+
async getById(
26+
workflowId: string,
27+
): Promise<{
28+
included: WorkflowIncludedResource[];
29+
workflow: CiWorkflow;
30+
}> {
31+
const response = await this.get<CiWorkflow, WorkflowIncludedResource>(
32+
`/v1/ciWorkflows/${workflowId}`,
33+
{
34+
include: 'repository,xcodeVersion,macOsVersion',
35+
},
36+
);
37+
38+
return {
39+
workflow: response.data,
40+
included: response.included ?? [],
41+
};
42+
}
2143
}

src/api/types.ts

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
/**
22
* App Store Connect API response envelope.
33
*/
4-
export interface APIResponse<TData> {
4+
export interface APIResponse<TData, TIncluded = never> {
55
data: TData;
6+
included?: TIncluded[];
67
links?: {
78
self?: string;
89
next?: string;
@@ -50,12 +51,105 @@ export interface CiWorkflow {
5051
name: string;
5152
description?: string;
5253
isEnabled: boolean;
54+
isLockedForEditing?: boolean;
5355
clean: boolean;
5456
containerFilePath: string;
5557
lastModifiedDate: string;
58+
branchStartCondition?: Record<string, unknown>;
59+
manualBranchStartCondition?: Record<string, unknown>;
60+
pullRequestStartCondition?: Record<string, unknown>;
61+
manualPullRequestStartCondition?: Record<string, unknown>;
62+
tagStartCondition?: Record<string, unknown>;
63+
manualTagStartCondition?: Record<string, unknown>;
64+
scheduledStartCondition?: Record<string, unknown>;
65+
actions?: CiWorkflowAction[];
66+
};
67+
relationships?: {
68+
repository?: {
69+
data?: {
70+
type: 'scmRepositories';
71+
id: string;
72+
} | null;
73+
};
74+
xcodeVersion?: {
75+
data?: {
76+
type: 'ciXcodeVersions';
77+
id: string;
78+
} | null;
79+
};
80+
macOsVersion?: {
81+
data?: {
82+
type: 'ciMacOsVersions';
83+
id: string;
84+
} | null;
85+
};
86+
};
87+
}
88+
89+
/**
90+
* Xcode Cloud workflow action.
91+
*/
92+
export interface CiWorkflowAction {
93+
actionType: string;
94+
buildDistributionAudience?: string | null;
95+
destination?: string | null;
96+
isRequiredToPass?: boolean | null;
97+
name: string;
98+
platform?: string | null;
99+
scheme?: string | null;
100+
testConfiguration?: {
101+
kind?: string | null;
102+
testDestinations?: Array<Record<string, unknown>>;
103+
testPlanName?: string | null;
104+
} | null;
105+
}
106+
107+
/**
108+
* Xcode Cloud source repository.
109+
*/
110+
export interface ScmRepository {
111+
type: 'scmRepositories';
112+
id: string;
113+
attributes: {
114+
defaultBranch?: string;
115+
httpCloneUrl?: string;
116+
ownerName?: string;
117+
repositoryName?: string;
118+
scmProvider?: string;
119+
sshCloneUrl?: string;
120+
};
121+
}
122+
123+
/**
124+
* Xcode version used by a workflow.
125+
*/
126+
export interface CiXcodeVersion {
127+
type: 'ciXcodeVersions';
128+
id: string;
129+
attributes: {
130+
name?: string;
131+
testDestinations?: Array<Record<string, unknown>>;
132+
version?: string;
56133
};
57134
}
58135

136+
/**
137+
* macOS version used by a workflow.
138+
*/
139+
export interface CiMacOsVersion {
140+
type: 'ciMacOsVersions';
141+
id: string;
142+
attributes: {
143+
name?: string;
144+
version?: string;
145+
};
146+
}
147+
148+
export type WorkflowIncludedResource =
149+
| CiMacOsVersion
150+
| CiXcodeVersion
151+
| ScmRepository;
152+
59153
export interface CiIssueCounts {
60154
analyzerWarnings: number;
61155
errors: number;

src/tools/discovery.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { z } from 'zod';
22
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
33
import type { AppStoreConnectClient } from '../api/client.js';
4+
import type {
5+
CiMacOsVersion,
6+
CiWorkflowAction,
7+
CiXcodeVersion,
8+
ScmRepository,
9+
WorkflowIncludedResource,
10+
} from '../api/types.js';
411
import { parseIdentifier } from '../utils/identifiers.js';
512
import { errorResponse, jsonResponse } from '../utils/tool-response.js';
613

@@ -70,4 +77,131 @@ export function registerDiscoveryTools(
7077
}
7178
},
7279
);
80+
81+
server.registerTool(
82+
'get_workflow_details',
83+
{
84+
description:
85+
'Retrieve detailed Xcode Cloud workflow configuration, including environment, start conditions, actions, and enabled state.',
86+
inputSchema: {
87+
workflowId: z.string(),
88+
},
89+
},
90+
async ({ workflowId }: { workflowId: string }) => {
91+
try {
92+
const workflowIdentifier = parseIdentifier(workflowId, 'workflow');
93+
const { workflow, included } = await client.workflows.getById(workflowIdentifier);
94+
const repository = findIncludedResource<ScmRepository>(
95+
included,
96+
workflow.relationships?.repository?.data?.id,
97+
'scmRepositories',
98+
);
99+
const xcodeVersion = findIncludedResource<CiXcodeVersion>(
100+
included,
101+
workflow.relationships?.xcodeVersion?.data?.id,
102+
'ciXcodeVersions',
103+
);
104+
const macOsVersion = findIncludedResource<CiMacOsVersion>(
105+
included,
106+
workflow.relationships?.macOsVersion?.data?.id,
107+
'ciMacOsVersions',
108+
);
109+
const actions = workflow.attributes.actions ?? [];
110+
111+
return jsonResponse({
112+
workflow: {
113+
id: workflow.id,
114+
general: {
115+
name: workflow.attributes.name,
116+
description: workflow.attributes.description ?? null,
117+
isEnabled: workflow.attributes.isEnabled,
118+
isLockedForEditing: workflow.attributes.isLockedForEditing ?? null,
119+
clean: workflow.attributes.clean,
120+
containerFilePath: workflow.attributes.containerFilePath,
121+
lastModifiedDate: workflow.attributes.lastModifiedDate,
122+
},
123+
environment: {
124+
repository: repository
125+
? {
126+
id: repository.id,
127+
ownerName: repository.attributes.ownerName ?? null,
128+
repositoryName: repository.attributes.repositoryName ?? null,
129+
scmProvider: repository.attributes.scmProvider ?? null,
130+
defaultBranch: repository.attributes.defaultBranch ?? null,
131+
httpCloneUrl: repository.attributes.httpCloneUrl ?? null,
132+
sshCloneUrl: repository.attributes.sshCloneUrl ?? null,
133+
}
134+
: null,
135+
xcodeVersion: xcodeVersion
136+
? {
137+
id: xcodeVersion.id,
138+
name: xcodeVersion.attributes.name ?? null,
139+
version: xcodeVersion.attributes.version ?? null,
140+
supportedTestDestinations:
141+
xcodeVersion.attributes.testDestinations?.length ?? 0,
142+
}
143+
: null,
144+
macOsVersion: macOsVersion
145+
? {
146+
id: macOsVersion.id,
147+
name: macOsVersion.attributes.name ?? null,
148+
version: macOsVersion.attributes.version ?? null,
149+
}
150+
: null,
151+
},
152+
startConditions: {
153+
branch: workflow.attributes.branchStartCondition ?? null,
154+
manualBranch: workflow.attributes.manualBranchStartCondition ?? null,
155+
pullRequest: workflow.attributes.pullRequestStartCondition ?? null,
156+
manualPullRequest:
157+
workflow.attributes.manualPullRequestStartCondition ?? null,
158+
scheduled: workflow.attributes.scheduledStartCondition ?? null,
159+
tag: workflow.attributes.tagStartCondition ?? null,
160+
manualTag: workflow.attributes.manualTagStartCondition ?? null,
161+
},
162+
actions: actions.map(formatWorkflowAction),
163+
postActions: [],
164+
postActionsNote:
165+
'The App Store Connect workflow payload did not expose separate post-actions, so this field is empty unless Apple adds that data.',
166+
},
167+
});
168+
} catch (error) {
169+
return errorResponse(error);
170+
}
171+
},
172+
);
173+
}
174+
175+
function findIncludedResource<TResource extends WorkflowIncludedResource>(
176+
resources: WorkflowIncludedResource[],
177+
identifier: string | undefined,
178+
type: TResource['type'],
179+
): TResource | undefined {
180+
if (!identifier) {
181+
return undefined;
182+
}
183+
184+
return resources.find(
185+
(resource): resource is TResource =>
186+
resource.id === identifier && resource.type === type,
187+
);
188+
}
189+
190+
function formatWorkflowAction(action: CiWorkflowAction) {
191+
return {
192+
name: action.name,
193+
actionType: action.actionType,
194+
platform: action.platform ?? null,
195+
scheme: action.scheme ?? null,
196+
destination: action.destination ?? null,
197+
buildDistributionAudience: action.buildDistributionAudience ?? null,
198+
isRequiredToPass: action.isRequiredToPass ?? null,
199+
testConfiguration: action.testConfiguration
200+
? {
201+
kind: action.testConfiguration.kind ?? null,
202+
testPlanName: action.testConfiguration.testPlanName ?? null,
203+
testDestinations: action.testConfiguration.testDestinations ?? [],
204+
}
205+
: null,
206+
};
73207
}

tests/smoke.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ test('server starts over stdio and exposes the expected tools', async () => {
3838
'get_failed_tests',
3939
'get_test_artifacts',
4040
'get_test_results',
41+
'get_workflow_details',
4142
'list_build_runs',
4243
'list_products',
4344
'list_workflows',

0 commit comments

Comments
 (0)