Skip to content

Commit 9d77774

Browse files
Merge pull request #156 from haifeng-li-at-salesforce/connectedAppCredential
feat: Add connected app credential workflow
2 parents c2275d3 + 1629fa7 commit 9d77774

29 files changed

Lines changed: 3044 additions & 1066 deletions

docs/5_mobile_native_app_generation.md

Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,20 @@ By the end of each Design/Iterate phase, the user validates implemented features
273273

274274
#### Connected App Configuration
275275

276-
- Gather required Connected App Client ID and Callback URI
277-
- Essential inputs for baseline mobile app project creation
276+
For MSDK apps (Mobile SDK templates without custom properties), the workflow automatically retrieves Connected App credentials from the user's Salesforce org:
277+
278+
1. **Org Discovery**: The workflow executes `sf org list --json` to discover all connected Salesforce orgs (DevHubs) available on the user's machine
279+
2. **Org Selection**: Presents the list of connected orgs to the user for selection
280+
3. **Connected App Discovery**: Executes `sf org list metadata -m ConnectedApp --json -o <selectedOrg>` to discover available Connected Apps in the selected org
281+
4. **Connected App Selection**: Presents the list of Connected Apps to the user for selection
282+
5. **Metadata Retrieval**: Retrieves the selected Connected App's metadata using `sf project retrieve start -m ConnectedApp:<appName> -o <selectedOrg>` in a temporary SFDX project
283+
6. **Credential Extraction**: Parses the XML metadata to extract the `consumerKey`, `callbackUrl`, and the org's `instanceUrl` (login host) for OAuth configuration
284+
285+
This approach is more secure than environment variables as it:
286+
- Lets the user choose which org to retrieve credentials from
287+
- Retrieves credentials directly from the selected org
288+
- Ensures credentials match actual Connected App configuration
289+
- Eliminates manual environment variable setup
278290

279291
#### Project Creation and Setup
280292

@@ -837,16 +849,24 @@ The StateGraph implements the three-phase architecture through deterministic nod
837849
const WorkflowStateAnnotation = Annotation.Root({
838850
// Core workflow data
839851
userInput: Annotation<unknown>,
852+
templatePropertiesUserInput: Annotation<unknown>,
840853
platform: Annotation<'iOS' | 'Android'>,
841854

842855
// Plan phase state
843-
validEnvironment: Annotation<boolean>,
856+
validPlatformSetup: Annotation<boolean>,
857+
validPluginSetup: Annotation<boolean>,
844858
workflowFatalErrorMessages: Annotation<string[]>,
845859
selectedTemplate: Annotation<string>,
860+
templateProperties: Annotation<Record<string, string>>,
861+
templatePropertiesMetadata: Annotation<TemplatePropertiesMetadata>,
846862
projectName: Annotation<string>,
847863
projectPath: Annotation<string>,
848864
packageName: Annotation<string>,
849865
organization: Annotation<string>,
866+
867+
// Connected App state (for MSDK apps)
868+
connectedAppList: Annotation<ConnectedAppInfo[]>,
869+
selectedConnectedAppName: Annotation<string>,
850870
connectedAppClientId: Annotation<string>,
851871
connectedAppCallbackUri: Annotation<string>,
852872
loginHost: Annotation<string>,
@@ -865,23 +885,39 @@ const WorkflowStateAnnotation = Annotation.Root({
865885

866886
const workflowGraph = new StateGraph(WorkflowStateAnnotation)
867887
// Workflow nodes (steel thread implementation)
868-
.addNode('validateEnvironment', environmentValidationNode.execute)
869888
.addNode('initialUserInputExtraction', initialUserInputExtractionNode.execute)
870889
.addNode('getUserInput', userInputNode.execute)
890+
.addNode('pluginCheck', pluginCheckNode.execute)
891+
.addNode('platformCheck', platformCheckNode.execute)
871892
.addNode('templateDiscovery', templateDiscoveryNode.execute)
893+
.addNode('templateSelection', templateSelectionNode.execute)
894+
.addNode('templatePropertiesExtraction', templatePropertiesExtractionNode.execute)
895+
.addNode('templatePropertiesUserInput', templatePropertiesUserInputNode.execute)
896+
// Connected app nodes (for MSDK apps)
897+
.addNode('fetchConnectedAppList', fetchConnectedAppListNode.execute)
898+
.addNode('selectConnectedApp', selectConnectedAppNode.execute)
899+
.addNode('retrieveConnectedAppMetadata', retrieveConnectedAppMetadataNode.execute)
872900
.addNode('projectGeneration', projectGenerationNode.execute)
873901
.addNode('buildValidation', buildValidationNode.execute)
874902
.addNode('buildRecovery', buildRecoveryNode.execute)
875903
.addNode('deployment', deploymentNode.execute)
876904
.addNode('completion', completionNode.execute)
877905
.addNode('failure', failureNode.execute)
878906

879-
// Define workflow edges
880-
.addEdge(START, 'validateEnvironment')
881-
.addConditionalEdges('validateEnvironment', checkEnvironmentValidatedRouter.execute)
907+
// Define workflow edges - start with user input extraction
908+
.addEdge(START, 'initialUserInputExtraction')
882909
.addConditionalEdges('initialUserInputExtraction', checkPropertiesFulFilledRouter.execute)
883910
.addEdge('getUserInput', 'initialUserInputExtraction')
884-
.addEdge('templateDiscovery', 'projectGeneration')
911+
.addConditionalEdges('pluginCheck', checkPluginValidatedRouter.execute)
912+
.addConditionalEdges('platformCheck', checkSetupValidatedRouter.execute)
913+
.addEdge('templateDiscovery', 'templateSelection')
914+
.addEdge('templateSelection', 'templatePropertiesExtraction')
915+
.addConditionalEdges('templatePropertiesExtraction', checkTemplatePropertiesFulfilledRouter.execute)
916+
.addEdge('templatePropertiesUserInput', 'templatePropertiesExtraction')
917+
// Connected app flow (for MSDK apps)
918+
.addEdge('fetchConnectedAppList', 'selectConnectedApp')
919+
.addEdge('selectConnectedApp', 'retrieveConnectedAppMetadata')
920+
.addConditionalEdges('retrieveConnectedAppMetadata', checkConnectedAppRetrievedRouter.execute)
885921
.addEdge('projectGeneration', 'buildValidation')
886922
// Build validation with recovery loop
887923
.addConditionalEdges('buildValidation', checkBuildSuccessfulRouter.execute)
@@ -892,19 +928,21 @@ const workflowGraph = new StateGraph(WorkflowStateAnnotation)
892928
.addEdge('failure', END);
893929

894930
// Example node implementations following the interrupt pattern
895-
// Environment validation (synchronous node - no interrupt)
896-
class EnvironmentValidationNode extends BaseNode {
897-
execute = (state: State): Partial<State> => {
898-
const { invalidEnvironmentMessages, connectedAppClientId, connectedAppCallbackUri } =
899-
this.validateEnvironmentVariables();
900-
901-
const validEnvironment = invalidEnvironmentMessages.length === 0;
902-
return {
903-
validEnvironment,
904-
workflowFatalErrorMessages: validEnvironment ? undefined : invalidEnvironmentMessages,
905-
connectedAppClientId,
906-
connectedAppCallbackUri,
907-
};
931+
// Connected App List Fetch (async node - fetches from Salesforce org)
932+
class FetchConnectedAppListNode extends BaseNode {
933+
execute = async (state: State): Promise<Partial<State>> => {
934+
// Execute sf org list metadata -m ConnectedApp --json
935+
const result = await this.commandRunner.execute('sf', [
936+
'org', 'list', 'metadata', '-m', 'ConnectedApp', '--json'
937+
]);
938+
939+
// Parse JSON and extract fullName and createdByName
940+
const connectedAppList = result.result.map(app => ({
941+
fullName: app.fullName,
942+
createdByName: app.createdByName,
943+
}));
944+
945+
return { connectedAppList };
908946
};
909947
}
910948

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright (c) 2025, salesforce.com, inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: MIT
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
6+
*/
7+
8+
import z from 'zod';
9+
import {
10+
WORKFLOW_TOOL_BASE_INPUT_SCHEMA,
11+
MCP_WORKFLOW_TOOL_OUTPUT_SCHEMA,
12+
WorkflowToolMetadata,
13+
} from '@salesforce/magen-mcp-workflow';
14+
15+
/**
16+
* Schema for Connected App information
17+
*/
18+
export const CONNECTED_APP_INFO_SCHEMA = z.object({
19+
fullName: z.string().describe('The API name of the Connected App'),
20+
createdByName: z.string().describe('The name of the user who created the Connected App'),
21+
});
22+
23+
export type ConnectedAppInfoInput = z.infer<typeof CONNECTED_APP_INFO_SCHEMA>;
24+
25+
/**
26+
* Connected App Selection Tool Input Schema
27+
*/
28+
export const CONNECTED_APP_SELECTION_WORKFLOW_INPUT_SCHEMA = WORKFLOW_TOOL_BASE_INPUT_SCHEMA.extend(
29+
{
30+
connectedAppList: z
31+
.array(CONNECTED_APP_INFO_SCHEMA)
32+
.describe('The list of Connected Apps available in the Salesforce org'),
33+
}
34+
);
35+
36+
export type ConnectedAppSelectionWorkflowInput = z.infer<
37+
typeof CONNECTED_APP_SELECTION_WORKFLOW_INPUT_SCHEMA
38+
>;
39+
40+
export const CONNECTED_APP_SELECTION_WORKFLOW_RESULT_SCHEMA = z.object({
41+
selectedConnectedAppName: z
42+
.string()
43+
.describe('The fullName of the Connected App selected by the user'),
44+
});
45+
46+
/**
47+
* Connected App Selection Tool Metadata
48+
*/
49+
export const CONNECTED_APP_SELECTION_TOOL: WorkflowToolMetadata<
50+
typeof CONNECTED_APP_SELECTION_WORKFLOW_INPUT_SCHEMA,
51+
typeof CONNECTED_APP_SELECTION_WORKFLOW_RESULT_SCHEMA
52+
> = {
53+
toolId: 'sfmobile-native-connected-app-selection',
54+
title: 'Salesforce Mobile Native Connected App Selection',
55+
description:
56+
'Guides user through selecting a Connected App from the available options in their Salesforce org',
57+
inputSchema: CONNECTED_APP_SELECTION_WORKFLOW_INPUT_SCHEMA,
58+
outputSchema: MCP_WORKFLOW_TOOL_OUTPUT_SCHEMA,
59+
resultSchema: CONNECTED_APP_SELECTION_WORKFLOW_RESULT_SCHEMA,
60+
} as const;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright (c) 2025, salesforce.com, inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: MIT
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
6+
*/
7+
8+
import z from 'zod';
9+
import {
10+
WORKFLOW_TOOL_BASE_INPUT_SCHEMA,
11+
MCP_WORKFLOW_TOOL_OUTPUT_SCHEMA,
12+
WorkflowToolMetadata,
13+
} from '@salesforce/magen-mcp-workflow';
14+
15+
/**
16+
* Schema for Salesforce org information
17+
*/
18+
export const ORG_INFO_SCHEMA = z.object({
19+
username: z.string().describe('The username of the Salesforce org'),
20+
alias: z.string().optional().describe('The alias of the Salesforce org'),
21+
});
22+
23+
export type OrgInfoInput = z.infer<typeof ORG_INFO_SCHEMA>;
24+
25+
/**
26+
* Org Selection Tool Input Schema
27+
*/
28+
export const ORG_SELECTION_WORKFLOW_INPUT_SCHEMA = WORKFLOW_TOOL_BASE_INPUT_SCHEMA.extend({
29+
orgList: z
30+
.array(ORG_INFO_SCHEMA)
31+
.describe('The list of connected Salesforce orgs available for selection'),
32+
});
33+
34+
export type OrgSelectionWorkflowInput = z.infer<typeof ORG_SELECTION_WORKFLOW_INPUT_SCHEMA>;
35+
36+
export const ORG_SELECTION_WORKFLOW_RESULT_SCHEMA = z.object({
37+
selectedOrgUsername: z
38+
.string()
39+
.describe('The username of the Salesforce org selected by the user'),
40+
});
41+
42+
/**
43+
* Org Selection Tool Metadata
44+
*/
45+
export const ORG_SELECTION_TOOL: WorkflowToolMetadata<
46+
typeof ORG_SELECTION_WORKFLOW_INPUT_SCHEMA,
47+
typeof ORG_SELECTION_WORKFLOW_RESULT_SCHEMA
48+
> = {
49+
toolId: 'sfmobile-native-org-selection',
50+
title: 'Salesforce Mobile Native Org Selection',
51+
description: 'Guides user through selecting a Salesforce org from the available connected orgs',
52+
inputSchema: ORG_SELECTION_WORKFLOW_INPUT_SCHEMA,
53+
outputSchema: MCP_WORKFLOW_TOOL_OUTPUT_SCHEMA,
54+
resultSchema: ORG_SELECTION_WORKFLOW_RESULT_SCHEMA,
55+
} as const;

packages/mobile-native-mcp-server/src/workflow/graph.ts

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
WORKFLOW_USER_INPUT_PROPERTIES,
1313
ANDROID_SETUP_PROPERTIES,
1414
} from './metadata.js';
15-
import { EnvironmentValidationNode } from './nodes/environment.js';
1615
import { TemplateOptionsFetchNode } from './nodes/templateOptionsFetch.js';
1716
import { TemplateSelectionNode } from './nodes/templateSelection.js';
1817
import { ProjectGenerationNode } from './nodes/projectGeneration.js';
@@ -23,7 +22,6 @@ import { CheckBuildSuccessfulRouter } from './nodes/checkBuildSuccessfulRouter.j
2322
import { DeploymentNode } from './nodes/deploymentNode.js';
2423
import { CompletionNode } from './nodes/completionNode.js';
2524
import { FailureNode } from './nodes/failureNode.js';
26-
import { CheckEnvironmentValidatedRouter } from './nodes/checkEnvironmentValidated.js';
2725
import { PlatformCheckNode } from './nodes/checkPlatformSetup.js';
2826
import { CheckSetupValidatedRouter } from './nodes/checkSetupValidatedRouter.js';
2927
import { TemplatePropertiesExtractionNode } from './nodes/templatePropertiesExtraction.js';
@@ -35,6 +33,14 @@ import { PluginCheckNode } from './nodes/checkPluginSetup.js';
3533
import { CheckPluginValidatedRouter } from './nodes/checkPluginValidatedRouter.js';
3634
import { CheckProjectGenerationRouter } from './nodes/checkProjectGenerationRouter.js';
3735
import { CheckDeploymentPlatformRouter } from './nodes/checkDeploymentPlatformRouter.js';
36+
import { FetchConnectedAppListNode } from './nodes/fetchConnectedAppList.js';
37+
import { SelectConnectedAppNode } from './nodes/selectConnectedApp.js';
38+
import { RetrieveConnectedAppMetadataNode } from './nodes/retrieveConnectedAppMetadata.js';
39+
import { CheckConnectedAppListRouter } from './nodes/checkConnectedAppListRouter.js';
40+
import { CheckConnectedAppRetrievedRouter } from './nodes/checkConnectedAppRetrievedRouter.js';
41+
import { FetchOrgsNode } from './nodes/fetchOrgs.js';
42+
import { SelectOrgNode } from './nodes/selectOrg.js';
43+
import { CheckOrgListRouter } from './nodes/checkOrgListRouter.js';
3844
import {
3945
createGetUserInputNode,
4046
createUserInputExtractionNode,
@@ -83,7 +89,6 @@ const getAndroidSetupNode = createGetUserInputNode<State>({
8389

8490
const extractAndroidSetupNode = new ExtractAndroidSetupNode();
8591

86-
const environmentValidationNode = new EnvironmentValidationNode();
8792
const platformCheckNode = new PlatformCheckNode();
8893
const pluginCheckNode = new PluginCheckNode();
8994
const templateOptionsFetchNode = new TemplateOptionsFetchNode();
@@ -99,10 +104,6 @@ const checkPropertiesFulFilledRouter = new CheckPropertiesFulfilledRouter<State>
99104
userInputNode.name,
100105
WORKFLOW_USER_INPUT_PROPERTIES
101106
);
102-
const checkEnvironmentValidatedRouter = new CheckEnvironmentValidatedRouter(
103-
initialUserInputExtractionNode.name,
104-
failureNode.name
105-
);
106107
const checkSetupValidatedRouter = new CheckSetupValidatedRouter(
107108
templateOptionsFetchNode.name,
108109
getAndroidSetupNode.name,
@@ -142,6 +143,18 @@ export function createMobileNativeWorkflow(logger?: Logger) {
142143
const androidInstallAppNode = new AndroidInstallAppNode(commandRunner, logger);
143144
const androidLaunchAppNode = new AndroidLaunchAppNode(commandRunner, logger);
144145

146+
// Create org selection nodes (for MSDK apps)
147+
const fetchOrgsNode = new FetchOrgsNode(commandRunner, logger);
148+
const selectOrgNode = new SelectOrgNode(undefined, logger);
149+
150+
// Create connected app nodes (for MSDK apps)
151+
const fetchConnectedAppListNode = new FetchConnectedAppListNode(commandRunner, logger);
152+
const selectConnectedAppNode = new SelectConnectedAppNode(undefined, logger);
153+
const retrieveConnectedAppMetadataNode = new RetrieveConnectedAppMetadataNode(
154+
commandRunner,
155+
logger
156+
);
157+
145158
// Create routers
146159
const checkProjectGenerationRouterInstance = new CheckProjectGenerationRouter(
147160
buildValidationNodeInstance.name,
@@ -156,7 +169,20 @@ export function createMobileNativeWorkflow(logger?: Logger) {
156169

157170
const checkTemplatePropertiesFulfilledRouter = new CheckTemplatePropertiesFulfilledRouter(
158171
projectGenerationNode.name,
159-
templatePropertiesUserInputNode.name
172+
templatePropertiesUserInputNode.name,
173+
fetchOrgsNode.name
174+
);
175+
176+
const checkOrgListRouter = new CheckOrgListRouter(selectOrgNode.name, failureNode.name);
177+
178+
const checkConnectedAppListRouter = new CheckConnectedAppListRouter(
179+
selectConnectedAppNode.name,
180+
failureNode.name
181+
);
182+
183+
const checkConnectedAppRetrievedRouter = new CheckConnectedAppRetrievedRouter(
184+
projectGenerationNode.name,
185+
failureNode.name
160186
);
161187

162188
const checkDeploymentPlatformRouterInstance = new CheckDeploymentPlatformRouter(
@@ -190,7 +216,6 @@ export function createMobileNativeWorkflow(logger?: Logger) {
190216
return (
191217
new StateGraph(MobileNativeWorkflowState)
192218
// Add all workflow nodes
193-
.addNode(environmentValidationNode.name, environmentValidationNode.execute)
194219
.addNode(initialUserInputExtractionNode.name, initialUserInputExtractionNode.execute)
195220
.addNode(userInputNode.name, userInputNode.execute)
196221
.addNode(platformCheckNode.name, platformCheckNode.execute)
@@ -201,6 +226,13 @@ export function createMobileNativeWorkflow(logger?: Logger) {
201226
.addNode(templateSelectionNode.name, templateSelectionNode.execute)
202227
.addNode(templatePropertiesExtractionNode.name, templatePropertiesExtractionNode.execute)
203228
.addNode(templatePropertiesUserInputNode.name, templatePropertiesUserInputNode.execute)
229+
// Org selection nodes (for MSDK apps)
230+
.addNode(fetchOrgsNode.name, fetchOrgsNode.execute)
231+
.addNode(selectOrgNode.name, selectOrgNode.execute)
232+
// Connected app nodes (for MSDK apps)
233+
.addNode(fetchConnectedAppListNode.name, fetchConnectedAppListNode.execute)
234+
.addNode(selectConnectedAppNode.name, selectConnectedAppNode.execute)
235+
.addNode(retrieveConnectedAppMetadataNode.name, retrieveConnectedAppMetadataNode.execute)
204236
.addNode(projectGenerationNode.name, projectGenerationNode.execute)
205237
.addNode(buildValidationNodeInstance.name, buildValidationNodeInstance.execute)
206238
.addNode(buildRecoveryNode.name, buildRecoveryNode.execute)
@@ -219,9 +251,8 @@ export function createMobileNativeWorkflow(logger?: Logger) {
219251
.addNode(completionNode.name, completionNode.execute)
220252
.addNode(failureNode.name, failureNode.execute)
221253

222-
// Define workflow edges
223-
.addEdge(START, environmentValidationNode.name)
224-
.addConditionalEdges(environmentValidationNode.name, checkEnvironmentValidatedRouter.execute)
254+
// Define workflow edges - start with user input extraction
255+
.addEdge(START, initialUserInputExtractionNode.name)
225256
.addConditionalEdges(
226257
initialUserInputExtractionNode.name,
227258
checkPropertiesFulFilledRouter.execute
@@ -239,6 +270,16 @@ export function createMobileNativeWorkflow(logger?: Logger) {
239270
checkTemplatePropertiesFulfilledRouter.execute
240271
)
241272
.addEdge(templatePropertiesUserInputNode.name, templatePropertiesExtractionNode.name)
273+
// Org selection flow (for MSDK apps)
274+
.addConditionalEdges(fetchOrgsNode.name, checkOrgListRouter.execute)
275+
.addEdge(selectOrgNode.name, fetchConnectedAppListNode.name)
276+
// Connected app flow (for MSDK apps)
277+
.addConditionalEdges(fetchConnectedAppListNode.name, checkConnectedAppListRouter.execute)
278+
.addEdge(selectConnectedAppNode.name, retrieveConnectedAppMetadataNode.name)
279+
.addConditionalEdges(
280+
retrieveConnectedAppMetadataNode.name,
281+
checkConnectedAppRetrievedRouter.execute
282+
)
242283
.addConditionalEdges(projectGenerationNode.name, checkProjectGenerationRouterInstance.execute)
243284
// Build validation with recovery loop (similar to user input loop)
244285
.addConditionalEdges(

0 commit comments

Comments
 (0)