Skip to content

Commit cf38b78

Browse files
Merge pull request #21 from forcedotcom/offlineAnalysis
Add Offline Analysis Tool for Mobile Web MCP
2 parents d9ef8c2 + c824585 commit cf38b78

File tree

39 files changed

+1815
-245
lines changed

39 files changed

+1815
-245
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ coverage/
1919
Thumbs.db
2020

2121

22-
22+
.sfdx/
2323
.cursor/rules/nx-rules.mdc
2424
.sfdx/
2525
.github/instructions/nx.instructions.md

package-lock.json

Lines changed: 756 additions & 96 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"packages/*"
88
],
99
"scripts": {
10-
"prettier:format": "prettier --write \"**/src/**/*.{ts, js, md, json}\" \"**/tests/**/*.{ts, js, md, json}\"",
10+
"prettier:format": "prettier --write \"**/src/**/*.{ts,js,md,json}\" \"**/tests/**/*.{ts,js,md,json}\" \"**/package.json\"",
1111
"prettier:verify": "prettier --list-different \"**/src/**/*.{ts, js, md, json}\" \"**/tests/**/*.{ts, js, md, json}\"",
1212
"lint": "eslint \"**/*.{ts,tsx,js,jsx}\"",
1313
"test": "nx run-many --target=test",
@@ -33,12 +33,11 @@
3333
"@modelcontextprotocol/sdk": "^1.12.3"
3434
},
3535
"devDependencies": {
36-
"@eslint/js": "^9.30.0",
3736
"@nx/workspace": "^21.2.1",
3837
"@types/node": "^24.0.10",
38+
"eslint": "^9.30.0",
3939
"@vitest/coverage-v8": "^3.2.3",
4040
"@typescript-eslint/eslint-plugin": "^8.35.1",
41-
"eslint": "^9.30.0",
4241
"eslint-config-prettier": "^10.1.5",
4342
"eslint-plugin-prettier": "^5.5.1",
4443
"husky": "^9.1.7",
@@ -49,8 +48,7 @@
4948
"tsx": "^4.19.2",
5049
"typescript": "^5.8.3",
5150
"typescript-eslint": "^8.35.1",
52-
"vite": "^6.3.5",
53-
"zod": "^3.25.67"
51+
"vite": "^6.3.5"
5452
},
5553
"nx": {
5654
"extends": "@nx/workspace/presets/npm.json"

packages/evaluation/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "@salesforce/mobile-mpc-tools-evaluation",
2+
"name": "@salesforce/mobile-mcp-tools-evaluation",
33
"version": "0.0.1",
44
"type": "module",
55
"files": [

packages/evaluation/project.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "@salesforce/mobile-mpc-tools-evaluation",
2+
"name": "@salesforce/mobile-mcp-tools-evaluation",
33
"$schema": "../../node_modules/nx/schemas/project-schema.json",
44
"sourceRoot": "packages/evaluation/src",
55
"projectType": "library",

packages/mobile-web/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"name": "@salesforce/mobile-web-mcp-server",
33
"version": "0.0.1",
4+
"type": "module",
45
"files": [
56
"dist",
67
"resources"
@@ -20,7 +21,11 @@
2021
"update-resources": "tsx src/scripts/update-type-declarations.ts"
2122
},
2223
"dependencies": {
23-
"@modelcontextprotocol/sdk": "^1.13.2"
24+
"@modelcontextprotocol/sdk": "^1.13.2",
25+
"eslint": "^9.30.0",
26+
"dedent": "^1.5.3",
27+
"zod": "^3.25.67",
28+
"@salesforce/eslint-plugin-lwc-graph-analyzer": "^1.0.0"
2429
},
2530
"devDependencies": {
2631
"@modelcontextprotocol/inspector": "^0.15.0",

packages/mobile-web/src/index.ts

Lines changed: 30 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,22 @@
99

1010
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1111
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12-
import { version } from '../package.json';
12+
import packageJson from '../package.json' with { type: 'json' };
13+
const version = packageJson.version;
1314
import { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';
1415

15-
import { AppReviewTool } from './tools/appReview/tool.js';
16-
import { ArSpaceCaptureTool } from './tools/arSpaceCapture/tool.js';
17-
import { BarcodeScannerTool } from './tools/barcodeScanner/tool.js';
18-
import { BiometricsTool } from './tools/biometrics/tool.js';
19-
import { CalendarTool } from './tools/calendar/tool.js';
20-
import { ContactsTool } from './tools/contacts/tool.js';
21-
import { DocumentScannerTool } from './tools/documentScanner/tool.js';
22-
import { GeofencingTool } from './tools/geofencing/tool.js';
23-
import { LocationTool } from './tools/location/tool.js';
24-
import { NfcTool } from './tools/nfc/tool.js';
25-
import { PaymentsTool } from './tools/payments/tool.js';
16+
import { AppReviewTool } from './tools/native-capabilities/appReview/tool.js';
17+
import { ArSpaceCaptureTool } from './tools/native-capabilities/arSpaceCapture/tool.js';
18+
import { BarcodeScannerTool } from './tools/native-capabilities/barcodeScanner/tool.js';
19+
import { BiometricsTool } from './tools/native-capabilities/biometrics/tool.js';
20+
import { CalendarTool } from './tools/native-capabilities/calendar/tool.js';
21+
import { ContactsTool } from './tools/native-capabilities/contacts/tool.js';
22+
import { DocumentScannerTool } from './tools/native-capabilities/documentScanner/tool.js';
23+
import { GeofencingTool } from './tools/native-capabilities/geofencing/tool.js';
24+
import { LocationTool } from './tools/native-capabilities/location/tool.js';
25+
import { NfcTool } from './tools/native-capabilities/nfc/tool.js';
26+
import { PaymentsTool } from './tools/native-capabilities/payments/tool.js';
27+
import { OfflineAnalysisTool } from './tools/mobile-offline/offline-analysis/tool.js';
2628

2729
const server = new McpServer({
2830
name: 'sfdc-mobile-web-mcp-server',
@@ -37,30 +39,23 @@ const annotations: ToolAnnotations = {
3739
openWorldHint: false,
3840
};
3941

40-
// Create and register all tools
41-
const appReviewTool = new AppReviewTool(server, annotations);
42-
const arSpaceCaptureTool = new ArSpaceCaptureTool(server, annotations);
43-
const barcodeScannerTool = new BarcodeScannerTool(server, annotations);
44-
const biometricsTool = new BiometricsTool(server, annotations);
45-
const calendarTool = new CalendarTool(server, annotations);
46-
const contactsTool = new ContactsTool(server, annotations);
47-
const documentScanner = new DocumentScannerTool(server, annotations);
48-
const geofencingTool = new GeofencingTool(server, annotations);
49-
const locationTool = new LocationTool(server, annotations);
50-
const nfcTool = new NfcTool(server, annotations);
51-
const paymentsTool = new PaymentsTool(server, annotations);
42+
const tools = [
43+
new AppReviewTool(),
44+
new ArSpaceCaptureTool(),
45+
new BarcodeScannerTool(),
46+
new BiometricsTool(),
47+
new CalendarTool(),
48+
new ContactsTool(),
49+
new DocumentScannerTool(),
50+
new GeofencingTool(),
51+
new LocationTool(),
52+
new NfcTool(),
53+
new PaymentsTool(),
54+
new OfflineAnalysisTool(),
55+
];
5256

53-
appReviewTool.register();
54-
arSpaceCaptureTool.register();
55-
barcodeScannerTool.register();
56-
biometricsTool.register();
57-
calendarTool.register();
58-
contactsTool.register();
59-
documentScanner.register();
60-
geofencingTool.register();
61-
locationTool.register();
62-
nfcTool.register();
63-
paymentsTool.register();
57+
// Register all tools
58+
tools.forEach(tool => tool.register(server, annotations));
6459

6560
export default server;
6661

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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+
import { z } from 'zod';
8+
9+
const ExpertReviewerNameSchema = z
10+
.string()
11+
.describe(
12+
'The title-cased name of the reviewer providing the review instructions, representing a brief description of the functional area meant to be reviewed.'
13+
);
14+
15+
export const CodeAnalysisBaseIssueSchema = z.object({
16+
type: z.string().describe('Categorize the issue'),
17+
description: z.string().describe('Why this is an issue?'),
18+
intentAnalysis: z.string().describe('What is the likely intent of the developer?'),
19+
suggestedAction: z
20+
.string()
21+
.describe(
22+
`How a developer should address the issue? Be as detailed as possible without adding code snippets.`
23+
),
24+
});
25+
26+
export const CodeAnalysisIssueSchema = CodeAnalysisBaseIssueSchema.extend({
27+
code: z.string().optional().describe('What is the code snippet with the issue?'),
28+
location: z
29+
.object({
30+
startLine: z.number().gte(0),
31+
endLine: z.number().gte(0).optional(),
32+
startColumn: z.number().gte(0).optional(),
33+
endColumn: z.number().gte(0).optional(),
34+
})
35+
.describe('Provide the exact line number(s) and column number(s) where the issue occurs'),
36+
});
37+
38+
export const ExpertCodeAnalysisIssuesSchema = z.object({
39+
expertReviewerName: ExpertReviewerNameSchema,
40+
issues: z
41+
.array(CodeAnalysisIssueSchema)
42+
.describe(
43+
'Specific issues found during the analysis. Should be empty if no issues were found.'
44+
),
45+
});
46+
47+
export const ExpertsCodeAnalysisIssuesSchema = z.object({
48+
analysisResults: z
49+
.array(ExpertCodeAnalysisIssuesSchema)
50+
.min(1)
51+
.describe(
52+
'An array of expert reviews of the code, each outlining specific issues found during analysis.'
53+
),
54+
orchestrationInstructions: z
55+
.string()
56+
.default(
57+
"If you have not already executed the 'sfmobile-web-offline-guidance' tool in this conversation thread: (1) Immediately call the 'sfmobile-web-offline-guidance' tool, (2) Execute all review instructions from the guidance tool's output against the user's LWC component code in the specified format, (3) Combine the review results from step 2 with this tool's analysis results, then refactor the component based on all combined review recommendations."
58+
)
59+
.describe(
60+
'Instructions for the MCP client on how to orchestrate this tool with the companion guidance tool for comprehensive offline compatibility review.'
61+
),
62+
});
63+
64+
export type CodeAnalysisIssueType = z.infer<typeof CodeAnalysisIssueSchema>;
65+
export type CodeAnalysisBaseIssueType = z.infer<typeof CodeAnalysisBaseIssueSchema>;
66+
export type ExpertCodeAnalysisIssuesType = z.infer<typeof ExpertCodeAnalysisIssuesSchema>;
67+
export type ExpertsCodeAnalysisIssuesType = z.infer<typeof ExpertsCodeAnalysisIssuesSchema>;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
import z from 'zod';
8+
9+
export const EmptySchema = z.object({});
10+
11+
const LwcFileSchema = z.object({
12+
path: z.string().describe('path to component file relative to LWC component bundle root'),
13+
content: z.string().describe('content of the file'),
14+
});
15+
16+
export const LwcCodeSchema = z.object({
17+
name: z.string().min(1).describe('Name of the LWC component'),
18+
namespace: z.string().describe('Namespace of the LWC component').default('c'),
19+
html: z.array(LwcFileSchema).min(1).describe('LWC component HTML templates.'),
20+
js: z.array(LwcFileSchema).min(1).describe('LWC component JavaScript files.'),
21+
css: z.array(LwcFileSchema).describe('LWC component CSS files.'),
22+
jsMetaXml: LwcFileSchema.describe('LWC component configuration .js-meta.xml file.'),
23+
});
24+
25+
export type LwcCodeType = z.TypeOf<typeof LwcCodeSchema>;
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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+
import { CodeAnalysisBaseIssueType } from '../../../schemas/analysisSchema.js';
8+
import dedent from 'dedent';
9+
10+
export interface RuleConfig {
11+
id: string; //ESLint rule id
12+
config: CodeAnalysisBaseIssueType;
13+
}
14+
// ********** Rules: no-private-wire-config-property **********
15+
const NO_PRIVATE_WIRE_CONFIG_RULE_ID =
16+
'@salesforce/lwc-graph-analyzer/no-private-wire-config-property';
17+
18+
const noPrivateWireRule: CodeAnalysisBaseIssueType = {
19+
type: 'Private Wire Configuration Property',
20+
description:
21+
'Properties used in wire configurations must be decorated with @api to be public and resolvable by the wire service.',
22+
intentAnalysis:
23+
'The developer used properties in wire configurations without making them public using the @api decorator.',
24+
25+
suggestedAction: dedent`
26+
Make the properties public by using the @api decorator:
27+
- Add @api decorator to properties used in wire configurations
28+
`,
29+
};
30+
31+
// ********** Rules: no-wire-config-references-non-local-property-reactive-value **********
32+
const NO_WIRE_CONFIG_REFERENCES_NON_LOCAL_PROPERTY_REACTIVE_VALUE_RULE_ID =
33+
'@salesforce/lwc-graph-analyzer/no-wire-config-references-non-local-property-reactive-value';
34+
35+
const noWireConfigReferenceNonLocalPropertyRule: CodeAnalysisBaseIssueType = {
36+
type: 'Wire Configuration References Non-Local Property',
37+
description:
38+
'Wire configurations with reactive values ($prop) must reference only component properties, not imported values or values defined outside the component class.',
39+
intentAnalysis:
40+
'The developer is trying to use a non-local value (imported or module-level) as a reactive parameter in a wire configuration.',
41+
42+
suggestedAction: dedent`
43+
Wrap the non-local value in a getter:
44+
- Introduce a getter which returns the imported value or the value of a module-level constant
45+
- Update the wire configuration to use the getter name as the reactive parameter
46+
Example:
47+
// Instead of:
48+
@wire(getData, { param: importedValue })
49+
50+
// Use:
51+
get localValue() {
52+
return importedValue;
53+
}
54+
@wire(getData, { param: '$localValue' })
55+
`,
56+
};
57+
58+
const noPrivateWireRuleConfig: RuleConfig = {
59+
id: NO_PRIVATE_WIRE_CONFIG_RULE_ID,
60+
config: noPrivateWireRule,
61+
};
62+
63+
const noWireConfigReferenceNonLocalPropertyRuleConfig: RuleConfig = {
64+
id: NO_WIRE_CONFIG_REFERENCES_NON_LOCAL_PROPERTY_REACTIVE_VALUE_RULE_ID,
65+
config: noWireConfigReferenceNonLocalPropertyRule,
66+
};
67+
68+
export const ruleConfigs: RuleConfig[] = [
69+
noPrivateWireRuleConfig,
70+
noWireConfigReferenceNonLocalPropertyRuleConfig,
71+
];

0 commit comments

Comments
 (0)