Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 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
Original file line number Diff line number Diff line change
Expand Up @@ -712,20 +712,20 @@ describe('CheckPropertiesFulfilledRouter', () => {
it('should match production graph configuration', () => {
// This tests the actual node names used in typical workflow graphs
const router = new CheckPropertiesFulfilledRouter<State>(
'templateDiscovery',
'selectTemplateCandidates',
'getUserInput',
TEST_PROPERTIES
);

// All properties fulfilled - should route to templateDiscovery
// All properties fulfilled - should route to selectTemplateCandidates (or platformCheckNode in actual graph)
const fulfilledState = createTestState({
platform: 'iOS',
projectName: 'MyApp',
packageName: 'com.test.myapp',
organization: 'TestOrg',
loginHost: 'https://login.salesforce.com',
});
expect(router.execute(fulfilledState)).toBe('templateDiscovery');
expect(router.execute(fulfilledState)).toBe('selectTemplateCandidates');

// Some properties missing - should route to getUserInput
const unfulfilledState = createTestState({
Expand Down
36 changes: 36 additions & 0 deletions packages/mobile-native-mcp-server/src/common/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,39 @@ export const PROJECT_PATH_FIELD = z.string().describe('Path to the mobile projec
* Project name field used in multiple tools
*/
export const PROJECT_NAME_FIELD = z.string().describe('Name for the mobile app project');

/**
* Template-related schemas
* Used for validating template discovery and selection data structures
*/

// Schema for template metadata - only require platform and displayName, allow everything else to passthrough
const TemplateMetadataSchema = z
.object({
platform: z.enum(['ios', 'android']),
})
.passthrough();

// Schema for individual template entry in the list - require metadata and path (template identifier)
// Note: path is not defined in template-schema.json (which only defines the metadata structure),
// but it IS present in the CLI output from `sf mobilesdk listtemplates --doc --json`
const TemplateEntrySchema = z
.object({
path: z
.string()
.describe(
'Template path/name identifier used for template selection and detail fetching. Present in CLI output but not in template-schema.json'
),
metadata: TemplateMetadataSchema,
})
.passthrough();

// Schema for the complete template list output structure - allow passthrough for flexibility
export const TEMPLATE_LIST_SCHEMA = z
.object({
templates: z.array(TemplateEntrySchema),
})
.passthrough();

// Type inferred from the schema
export type TemplateListOutput = z.infer<typeof TEMPLATE_LIST_SCHEMA>;
6 changes: 3 additions & 3 deletions packages/mobile-native-mcp-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { SFMobileNativeTemplateDiscoveryTool } from './tools/plan/sfmobile-native-template-discovery/tool.js';
import { SFMobileNativeTemplateSelectionTool } from './tools/plan/sfmobile-native-template-selection/tool.js';
import { UtilsXcodeAddFilesTool } from './tools/utils/utils-xcode-add-files/tool.js';

import { SFMobileNativeDeploymentTool } from './tools/run/sfmobile-native-deployment/tool.js';
Expand Down Expand Up @@ -52,7 +52,7 @@ const orchestratorAnnotations: ToolAnnotations = {
const orchestrator = new MobileNativeOrchestrator(server);
const getInputTool = createSFMobileNativeGetInputTool(server);
const inputExtractionTool = createSFMobileNativeInputExtractionTool(server);
const templateDiscoveryTool = new SFMobileNativeTemplateDiscoveryTool(server);
const templateSelectionTool = new SFMobileNativeTemplateSelectionTool(server);
const projectGenerationTool = new SFMobileNativeProjectGenerationTool(server);
const buildTool = new SFMobileNativeBuildTool(server);
const buildRecoveryTool = new SFMobileNativeBuildRecoveryTool(server);
Expand All @@ -73,7 +73,7 @@ orchestrator.register(orchestratorAnnotations);
// Register all other tools with read-only annotations
getInputTool.register(readOnlyAnnotations);
inputExtractionTool.register(readOnlyAnnotations);
templateDiscoveryTool.register(readOnlyAnnotations);
templateSelectionTool.register(readOnlyAnnotations);
projectGenerationTool.register(readOnlyAnnotations);
buildTool.register(readOnlyAnnotations);
buildRecoveryTool.register(readOnlyAnnotations);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export const PROJECT_GENERATION_WORKFLOW_INPUT_SCHEMA = WORKFLOW_TOOL_BASE_INPUT
.string()
.optional()
.describe('Optional Salesforce login host URL (e.g., https://test.salesforce.com for sandbox)'),
templateProperties: z
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in a separate PR I want to address the similarities (and differences) between templateProperties and other app properties (i.e. connectedAppCallback, loginHost, etc) - I think ALL properties should be gathered in a similar fashion. Right now the system is very biased towards this being a mobilesdk app but in reality alot of B2C mobile sdk apps won't need loginHost, connectedAppClientId, etc and it should be the template metadata which describes if these properties should be collected at all

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To expand on the comment, MobileNativeWorkflowState can be split into two sections: One for MSDK-essential state and another for app-specific state.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I think we're going to have to think holistically about state as the workflow grows - we have a lot of state which is transient and just a means to carry data from one node to the next and then we're done with it so we should have a strategy where we clear state when we are done with that part of the workflow - @khawkins we've discussed this briefly, might be worth a targeted convo as we're going to continually hit it with our various workflows as they grow

.record(z.string())
.optional()
.describe('Custom template-specific properties required by the selected template'),
});

export type ProjectGenerationWorkflowInput = z.infer<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,9 @@ export class SFMobileNativeProjectGenerationTool extends AbstractNativeProjectMa

${this.generateStepExecuteCliCommand(1, input)}

${this.generateStepVerifyProjectStructure(2, input)}

## Success Criteria

✅ Project generated successfully from template "${input.selectedTemplate}"
✅ Project structure verified
`;
}

Expand All @@ -67,36 +64,27 @@ export class SFMobileNativeProjectGenerationTool extends AbstractNativeProjectMa
): string {
const platformLower = input.platform.toLowerCase();

// Build template properties flags if they exist
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think @haifeng-li-at-salesforce has a PR that puts this behind a node tool (which is great!) but we'll need to reconcile with these changes

let templatePropertiesFlags = '';
if (input.templateProperties && Object.keys(input.templateProperties).length > 0) {
const propertyFlags = Object.entries(input.templateProperties)
.map(([key, value]) => `--template-property-${key}="${value}"`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we generate the shell command to run. Do we need to worry the command line to be valid in case value has special character for shell, like " here for the value?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@haifeng-li-at-salesforce is doing work to move this work into a node I wonder if that's a more appropriate place to do deterministic validation since we'll also have access to the shell result - what do you think @haifeng-li-at-salesforce ? seems like doing that work within the tool itself is not appropriate - can you add it to your pr when resolving to do some basic validation on the input?

.join(' ');
templatePropertiesFlags = ` ${propertyFlags}`;
}

return dedent`
## Step ${stepNumber}: Execute Platform-Specific CLI Command

Generate the project using the Salesforce Mobile SDK CLI:

\`\`\`bash
sf mobilesdk ${platformLower} createwithtemplate --templatesource="${MOBILE_SDK_TEMPLATES_PATH}" --template="${input.selectedTemplate}" --appname="${input.projectName}" --packagename="${input.packageName}" --organization="${input.organization} --consumerkey="${input.connectedAppClientId}" --callbackurl="${input.connectedAppCallbackUri}" --loginserver="${input.loginHost}"
sf mobilesdk ${platformLower} createwithtemplate --templatesource="${MOBILE_SDK_TEMPLATES_PATH}" --template="${input.selectedTemplate}" --appname="${input.projectName}" --packagename="${input.packageName}" --organization="${input.organization} --consumerkey="${input.connectedAppClientId}" --callbackurl="${input.connectedAppCallbackUri}" --loginserver="${input.loginHost} ${templatePropertiesFlags}"
\`\`\`

**Expected Outcome**: A new ${input.platform} project directory named "${input.projectName}" will be created with the template structure. The output of the command will indicate the location of the bootconfig.plist file, take note of this for oauth configuration!

NOTE: it is VERY IMPORTANT to use the above command EXACTLY to generate the project. Do not use any other configuration method to generate the project. If the above command fails do not try to generate the project using any other method. Instead report back error to the user.
`;
}

private generateStepVerifyProjectStructure(
stepNumber: number,
input: ProjectGenerationWorkflowInput
): string {
return dedent`
## Step ${stepNumber}: Verify Project Structure

Navigate to the project directory and verify the basic structure:

\`\`\`bash
cd "${input.projectName}"
ls -la
\`\`\`

**Expected Structure**: You should see platform-specific files and directories appropriate for ${input.platform} development.
`;
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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 { PLATFORM_ENUM, TEMPLATE_LIST_SCHEMA } from '../../../common/schemas.js';
import {
WORKFLOW_TOOL_BASE_INPUT_SCHEMA,
MCP_WORKFLOW_TOOL_OUTPUT_SCHEMA,
WorkflowToolMetadata,
} from '@salesforce/magen-mcp-workflow';

/**
* Template Selection Tool Input Schema
*/
export const TEMPLATE_SELECTION_WORKFLOW_INPUT_SCHEMA = WORKFLOW_TOOL_BASE_INPUT_SCHEMA.extend({
platform: PLATFORM_ENUM,
templateOptions: TEMPLATE_LIST_SCHEMA.describe(
'The template options from listtemplates command. Each template includes metadata with platform, displayName, and other descriptive information.'
),
});

export type TemplateSelectionWorkflowInput = z.infer<
typeof TEMPLATE_SELECTION_WORKFLOW_INPUT_SCHEMA
>;

export const TEMPLATE_SELECTION_WORKFLOW_RESULT_SCHEMA = z.object({
selectedTemplate: z
.string()
.describe('The template path/name selected from the available templates'),
});

/**
* Template Selection Tool Metadata
*/
export const TEMPLATE_SELECTION_TOOL: WorkflowToolMetadata<
typeof TEMPLATE_SELECTION_WORKFLOW_INPUT_SCHEMA,
typeof TEMPLATE_SELECTION_WORKFLOW_RESULT_SCHEMA
> = {
toolId: 'sfmobile-native-template-selection',
title: 'Salesforce Mobile Native Template Selection',
description: 'Guides LLM through template selection from available template options',
inputSchema: TEMPLATE_SELECTION_WORKFLOW_INPUT_SCHEMA,
outputSchema: MCP_WORKFLOW_TOOL_OUTPUT_SCHEMA,
resultSchema: TEMPLATE_SELECTION_WORKFLOW_RESULT_SCHEMA,
} as const;
Loading
Loading