Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 59 additions & 21 deletions docs/5_mobile_native_app_generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,8 +273,20 @@ By the end of each Design/Iterate phase, the user validates implemented features

#### Connected App Configuration

- Gather required Connected App Client ID and Callback URI
- Essential inputs for baseline mobile app project creation
For MSDK apps (Mobile SDK templates without custom properties), the workflow automatically retrieves Connected App credentials from the user's Salesforce org:

1. **Org Discovery**: The workflow executes `sf org list --json` to discover all connected Salesforce orgs (DevHubs) available on the user's machine
2. **Org Selection**: Presents the list of connected orgs to the user for selection
3. **Connected App Discovery**: Executes `sf org list metadata -m ConnectedApp --json -o <selectedOrg>` to discover available Connected Apps in the selected org
4. **Connected App Selection**: Presents the list of Connected Apps to the user for selection
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
6. **Credential Extraction**: Parses the XML metadata to extract the `consumerKey`, `callbackUrl`, and the org's `instanceUrl` (login host) for OAuth configuration

This approach is more secure than environment variables as it:
- Lets the user choose which org to retrieve credentials from
- Retrieves credentials directly from the selected org
- Ensures credentials match actual Connected App configuration
- Eliminates manual environment variable setup

#### Project Creation and Setup

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

// Plan phase state
validEnvironment: Annotation<boolean>,
validPlatformSetup: Annotation<boolean>,
validPluginSetup: Annotation<boolean>,
workflowFatalErrorMessages: Annotation<string[]>,
selectedTemplate: Annotation<string>,
templateProperties: Annotation<Record<string, string>>,
templatePropertiesMetadata: Annotation<TemplatePropertiesMetadata>,
projectName: Annotation<string>,
projectPath: Annotation<string>,
packageName: Annotation<string>,
organization: Annotation<string>,

// Connected App state (for MSDK apps)
connectedAppList: Annotation<ConnectedAppInfo[]>,
selectedConnectedAppName: Annotation<string>,
connectedAppClientId: Annotation<string>,
connectedAppCallbackUri: Annotation<string>,
loginHost: Annotation<string>,
Expand All @@ -865,23 +885,39 @@ const WorkflowStateAnnotation = Annotation.Root({

const workflowGraph = new StateGraph(WorkflowStateAnnotation)
// Workflow nodes (steel thread implementation)
.addNode('validateEnvironment', environmentValidationNode.execute)
.addNode('initialUserInputExtraction', initialUserInputExtractionNode.execute)
.addNode('getUserInput', userInputNode.execute)
.addNode('pluginCheck', pluginCheckNode.execute)
.addNode('platformCheck', platformCheckNode.execute)
.addNode('templateDiscovery', templateDiscoveryNode.execute)
.addNode('templateSelection', templateSelectionNode.execute)
.addNode('templatePropertiesExtraction', templatePropertiesExtractionNode.execute)
.addNode('templatePropertiesUserInput', templatePropertiesUserInputNode.execute)
// Connected app nodes (for MSDK apps)
.addNode('fetchConnectedAppList', fetchConnectedAppListNode.execute)
.addNode('selectConnectedApp', selectConnectedAppNode.execute)
.addNode('retrieveConnectedAppMetadata', retrieveConnectedAppMetadataNode.execute)
.addNode('projectGeneration', projectGenerationNode.execute)
.addNode('buildValidation', buildValidationNode.execute)
.addNode('buildRecovery', buildRecoveryNode.execute)
.addNode('deployment', deploymentNode.execute)
.addNode('completion', completionNode.execute)
.addNode('failure', failureNode.execute)

// Define workflow edges
.addEdge(START, 'validateEnvironment')
.addConditionalEdges('validateEnvironment', checkEnvironmentValidatedRouter.execute)
// Define workflow edges - start with user input extraction
.addEdge(START, 'initialUserInputExtraction')
.addConditionalEdges('initialUserInputExtraction', checkPropertiesFulFilledRouter.execute)
.addEdge('getUserInput', 'initialUserInputExtraction')
.addEdge('templateDiscovery', 'projectGeneration')
.addConditionalEdges('pluginCheck', checkPluginValidatedRouter.execute)
.addConditionalEdges('platformCheck', checkSetupValidatedRouter.execute)
.addEdge('templateDiscovery', 'templateSelection')
.addEdge('templateSelection', 'templatePropertiesExtraction')
.addConditionalEdges('templatePropertiesExtraction', checkTemplatePropertiesFulfilledRouter.execute)
.addEdge('templatePropertiesUserInput', 'templatePropertiesExtraction')
// Connected app flow (for MSDK apps)
.addEdge('fetchConnectedAppList', 'selectConnectedApp')
.addEdge('selectConnectedApp', 'retrieveConnectedAppMetadata')
.addConditionalEdges('retrieveConnectedAppMetadata', checkConnectedAppRetrievedRouter.execute)
.addEdge('projectGeneration', 'buildValidation')
// Build validation with recovery loop
.addConditionalEdges('buildValidation', checkBuildSuccessfulRouter.execute)
Expand All @@ -892,19 +928,21 @@ const workflowGraph = new StateGraph(WorkflowStateAnnotation)
.addEdge('failure', END);

// Example node implementations following the interrupt pattern
// Environment validation (synchronous node - no interrupt)
class EnvironmentValidationNode extends BaseNode {
execute = (state: State): Partial<State> => {
const { invalidEnvironmentMessages, connectedAppClientId, connectedAppCallbackUri } =
this.validateEnvironmentVariables();

const validEnvironment = invalidEnvironmentMessages.length === 0;
return {
validEnvironment,
workflowFatalErrorMessages: validEnvironment ? undefined : invalidEnvironmentMessages,
connectedAppClientId,
connectedAppCallbackUri,
};
// Connected App List Fetch (async node - fetches from Salesforce org)
class FetchConnectedAppListNode extends BaseNode {
execute = async (state: State): Promise<Partial<State>> => {
// Execute sf org list metadata -m ConnectedApp --json
const result = await this.commandRunner.execute('sf', [
'org', 'list', 'metadata', '-m', 'ConnectedApp', '--json'
]);

// Parse JSON and extract fullName and createdByName
const connectedAppList = result.result.map(app => ({
fullName: app.fullName,
createdByName: app.createdByName,
}));

return { connectedAppList };
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright (c) 2025, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/

import z from 'zod';
import {
WORKFLOW_TOOL_BASE_INPUT_SCHEMA,
MCP_WORKFLOW_TOOL_OUTPUT_SCHEMA,
WorkflowToolMetadata,
} from '@salesforce/magen-mcp-workflow';

/**
* Schema for Connected App information
*/
export const CONNECTED_APP_INFO_SCHEMA = z.object({
fullName: z.string().describe('The API name of the Connected App'),
createdByName: z.string().describe('The name of the user who created the Connected App'),
});

export type ConnectedAppInfoInput = z.infer<typeof CONNECTED_APP_INFO_SCHEMA>;

/**
* Connected App Selection Tool Input Schema
*/
export const CONNECTED_APP_SELECTION_WORKFLOW_INPUT_SCHEMA = WORKFLOW_TOOL_BASE_INPUT_SCHEMA.extend(
{
connectedAppList: z
.array(CONNECTED_APP_INFO_SCHEMA)
.describe('The list of Connected Apps available in the Salesforce org'),
}
);

export type ConnectedAppSelectionWorkflowInput = z.infer<
typeof CONNECTED_APP_SELECTION_WORKFLOW_INPUT_SCHEMA
>;

export const CONNECTED_APP_SELECTION_WORKFLOW_RESULT_SCHEMA = z.object({
selectedConnectedAppName: z
.string()
.describe('The fullName of the Connected App selected by the user'),
});

/**
* Connected App Selection Tool Metadata
*/
export const CONNECTED_APP_SELECTION_TOOL: WorkflowToolMetadata<
typeof CONNECTED_APP_SELECTION_WORKFLOW_INPUT_SCHEMA,
typeof CONNECTED_APP_SELECTION_WORKFLOW_RESULT_SCHEMA
> = {
toolId: 'sfmobile-native-connected-app-selection',
title: 'Salesforce Mobile Native Connected App Selection',
description:
'Guides user through selecting a Connected App from the available options in their Salesforce org',
inputSchema: CONNECTED_APP_SELECTION_WORKFLOW_INPUT_SCHEMA,
outputSchema: MCP_WORKFLOW_TOOL_OUTPUT_SCHEMA,
resultSchema: CONNECTED_APP_SELECTION_WORKFLOW_RESULT_SCHEMA,
} as const;
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright (c) 2025, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/

import z from 'zod';
import {
WORKFLOW_TOOL_BASE_INPUT_SCHEMA,
MCP_WORKFLOW_TOOL_OUTPUT_SCHEMA,
WorkflowToolMetadata,
} from '@salesforce/magen-mcp-workflow';

/**
* Schema for Salesforce org information
*/
export const ORG_INFO_SCHEMA = z.object({
username: z.string().describe('The username of the Salesforce org'),
alias: z.string().optional().describe('The alias of the Salesforce org'),
});

export type OrgInfoInput = z.infer<typeof ORG_INFO_SCHEMA>;

/**
* Org Selection Tool Input Schema
*/
export const ORG_SELECTION_WORKFLOW_INPUT_SCHEMA = WORKFLOW_TOOL_BASE_INPUT_SCHEMA.extend({
orgList: z
.array(ORG_INFO_SCHEMA)
.describe('The list of connected Salesforce orgs available for selection'),
});

export type OrgSelectionWorkflowInput = z.infer<typeof ORG_SELECTION_WORKFLOW_INPUT_SCHEMA>;

export const ORG_SELECTION_WORKFLOW_RESULT_SCHEMA = z.object({
selectedOrgUsername: z
.string()
.describe('The username of the Salesforce org selected by the user'),
});

/**
* Org Selection Tool Metadata
*/
export const ORG_SELECTION_TOOL: WorkflowToolMetadata<
typeof ORG_SELECTION_WORKFLOW_INPUT_SCHEMA,
typeof ORG_SELECTION_WORKFLOW_RESULT_SCHEMA
> = {
toolId: 'sfmobile-native-org-selection',
title: 'Salesforce Mobile Native Org Selection',
description: 'Guides user through selecting a Salesforce org from the available connected orgs',
inputSchema: ORG_SELECTION_WORKFLOW_INPUT_SCHEMA,
outputSchema: MCP_WORKFLOW_TOOL_OUTPUT_SCHEMA,
resultSchema: ORG_SELECTION_WORKFLOW_RESULT_SCHEMA,
} as const;
65 changes: 53 additions & 12 deletions packages/mobile-native-mcp-server/src/workflow/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
WORKFLOW_USER_INPUT_PROPERTIES,
ANDROID_SETUP_PROPERTIES,
} from './metadata.js';
import { EnvironmentValidationNode } from './nodes/environment.js';
import { TemplateOptionsFetchNode } from './nodes/templateOptionsFetch.js';
import { TemplateSelectionNode } from './nodes/templateSelection.js';
import { ProjectGenerationNode } from './nodes/projectGeneration.js';
Expand All @@ -23,7 +22,6 @@ import { CheckBuildSuccessfulRouter } from './nodes/checkBuildSuccessfulRouter.j
import { DeploymentNode } from './nodes/deploymentNode.js';
import { CompletionNode } from './nodes/completionNode.js';
import { FailureNode } from './nodes/failureNode.js';
import { CheckEnvironmentValidatedRouter } from './nodes/checkEnvironmentValidated.js';
import { PlatformCheckNode } from './nodes/checkPlatformSetup.js';
import { CheckSetupValidatedRouter } from './nodes/checkSetupValidatedRouter.js';
import { TemplatePropertiesExtractionNode } from './nodes/templatePropertiesExtraction.js';
Expand All @@ -35,6 +33,14 @@ import { PluginCheckNode } from './nodes/checkPluginSetup.js';
import { CheckPluginValidatedRouter } from './nodes/checkPluginValidatedRouter.js';
import { CheckProjectGenerationRouter } from './nodes/checkProjectGenerationRouter.js';
import { CheckDeploymentPlatformRouter } from './nodes/checkDeploymentPlatformRouter.js';
import { FetchConnectedAppListNode } from './nodes/fetchConnectedAppList.js';
import { SelectConnectedAppNode } from './nodes/selectConnectedApp.js';
import { RetrieveConnectedAppMetadataNode } from './nodes/retrieveConnectedAppMetadata.js';
import { CheckConnectedAppListRouter } from './nodes/checkConnectedAppListRouter.js';
import { CheckConnectedAppRetrievedRouter } from './nodes/checkConnectedAppRetrievedRouter.js';
import { FetchOrgsNode } from './nodes/fetchOrgs.js';
import { SelectOrgNode } from './nodes/selectOrg.js';
import { CheckOrgListRouter } from './nodes/checkOrgListRouter.js';
import {
createGetUserInputNode,
createUserInputExtractionNode,
Expand Down Expand Up @@ -83,7 +89,6 @@ const getAndroidSetupNode = createGetUserInputNode<State>({

const extractAndroidSetupNode = new ExtractAndroidSetupNode();

const environmentValidationNode = new EnvironmentValidationNode();
const platformCheckNode = new PlatformCheckNode();
const pluginCheckNode = new PluginCheckNode();
const templateOptionsFetchNode = new TemplateOptionsFetchNode();
Expand All @@ -99,10 +104,6 @@ const checkPropertiesFulFilledRouter = new CheckPropertiesFulfilledRouter<State>
userInputNode.name,
WORKFLOW_USER_INPUT_PROPERTIES
);
const checkEnvironmentValidatedRouter = new CheckEnvironmentValidatedRouter(
initialUserInputExtractionNode.name,
failureNode.name
);
const checkSetupValidatedRouter = new CheckSetupValidatedRouter(
templateOptionsFetchNode.name,
getAndroidSetupNode.name,
Expand Down Expand Up @@ -142,6 +143,18 @@ export function createMobileNativeWorkflow(logger?: Logger) {
const androidInstallAppNode = new AndroidInstallAppNode(commandRunner, logger);
const androidLaunchAppNode = new AndroidLaunchAppNode(commandRunner, logger);

// Create org selection nodes (for MSDK apps)
const fetchOrgsNode = new FetchOrgsNode(commandRunner, logger);
const selectOrgNode = new SelectOrgNode(undefined, logger);

// Create connected app nodes (for MSDK apps)
const fetchConnectedAppListNode = new FetchConnectedAppListNode(commandRunner, logger);
const selectConnectedAppNode = new SelectConnectedAppNode(undefined, logger);
const retrieveConnectedAppMetadataNode = new RetrieveConnectedAppMetadataNode(
commandRunner,
logger
);

// Create routers
const checkProjectGenerationRouterInstance = new CheckProjectGenerationRouter(
buildValidationNodeInstance.name,
Expand All @@ -156,7 +169,20 @@ export function createMobileNativeWorkflow(logger?: Logger) {

const checkTemplatePropertiesFulfilledRouter = new CheckTemplatePropertiesFulfilledRouter(
projectGenerationNode.name,
templatePropertiesUserInputNode.name
templatePropertiesUserInputNode.name,
fetchOrgsNode.name
);

const checkOrgListRouter = new CheckOrgListRouter(selectOrgNode.name, failureNode.name);

const checkConnectedAppListRouter = new CheckConnectedAppListRouter(
selectConnectedAppNode.name,
failureNode.name
);

const checkConnectedAppRetrievedRouter = new CheckConnectedAppRetrievedRouter(
projectGenerationNode.name,
failureNode.name
);

const checkDeploymentPlatformRouterInstance = new CheckDeploymentPlatformRouter(
Expand Down Expand Up @@ -190,7 +216,6 @@ export function createMobileNativeWorkflow(logger?: Logger) {
return (
new StateGraph(MobileNativeWorkflowState)
// Add all workflow nodes
.addNode(environmentValidationNode.name, environmentValidationNode.execute)
.addNode(initialUserInputExtractionNode.name, initialUserInputExtractionNode.execute)
.addNode(userInputNode.name, userInputNode.execute)
.addNode(platformCheckNode.name, platformCheckNode.execute)
Expand All @@ -201,6 +226,13 @@ export function createMobileNativeWorkflow(logger?: Logger) {
.addNode(templateSelectionNode.name, templateSelectionNode.execute)
.addNode(templatePropertiesExtractionNode.name, templatePropertiesExtractionNode.execute)
.addNode(templatePropertiesUserInputNode.name, templatePropertiesUserInputNode.execute)
// Org selection nodes (for MSDK apps)
.addNode(fetchOrgsNode.name, fetchOrgsNode.execute)
.addNode(selectOrgNode.name, selectOrgNode.execute)
// Connected app nodes (for MSDK apps)
.addNode(fetchConnectedAppListNode.name, fetchConnectedAppListNode.execute)
.addNode(selectConnectedAppNode.name, selectConnectedAppNode.execute)
.addNode(retrieveConnectedAppMetadataNode.name, retrieveConnectedAppMetadataNode.execute)
.addNode(projectGenerationNode.name, projectGenerationNode.execute)
.addNode(buildValidationNodeInstance.name, buildValidationNodeInstance.execute)
.addNode(buildRecoveryNode.name, buildRecoveryNode.execute)
Expand All @@ -219,9 +251,8 @@ export function createMobileNativeWorkflow(logger?: Logger) {
.addNode(completionNode.name, completionNode.execute)
.addNode(failureNode.name, failureNode.execute)

// Define workflow edges
.addEdge(START, environmentValidationNode.name)
.addConditionalEdges(environmentValidationNode.name, checkEnvironmentValidatedRouter.execute)
// Define workflow edges - start with user input extraction
.addEdge(START, initialUserInputExtractionNode.name)
.addConditionalEdges(
initialUserInputExtractionNode.name,
checkPropertiesFulFilledRouter.execute
Expand All @@ -239,6 +270,16 @@ export function createMobileNativeWorkflow(logger?: Logger) {
checkTemplatePropertiesFulfilledRouter.execute
)
.addEdge(templatePropertiesUserInputNode.name, templatePropertiesExtractionNode.name)
// Org selection flow (for MSDK apps)
.addConditionalEdges(fetchOrgsNode.name, checkOrgListRouter.execute)
.addEdge(selectOrgNode.name, fetchConnectedAppListNode.name)
// Connected app flow (for MSDK apps)
.addConditionalEdges(fetchConnectedAppListNode.name, checkConnectedAppListRouter.execute)
.addEdge(selectConnectedAppNode.name, retrieveConnectedAppMetadataNode.name)
.addConditionalEdges(
retrieveConnectedAppMetadataNode.name,
checkConnectedAppRetrievedRouter.execute
)
.addConditionalEdges(projectGenerationNode.name, checkProjectGenerationRouterInstance.execute)
// Build validation with recovery loop (similar to user input loop)
.addConditionalEdges(
Expand Down
Loading