ios-app-review-plugin 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/.claude/settings.local.json +42 -0
  2. package/.github/actions/ios-review/action.yml +106 -0
  3. package/.github/workflows/ci.yml +103 -0
  4. package/.github/workflows/publish.yml +57 -0
  5. package/CHANGELOG.md +66 -0
  6. package/CONTRIBUTING.md +175 -0
  7. package/LICENSE +21 -0
  8. package/README.md +205 -0
  9. package/bitrise/step.sh +128 -0
  10. package/bitrise/step.yml +101 -0
  11. package/dist/analyzer.d.ts.map +1 -0
  12. package/dist/analyzers/asc-iap.d.ts.map +1 -0
  13. package/dist/analyzers/asc-metadata.d.ts.map +1 -0
  14. package/dist/analyzers/asc-screenshots.d.ts.map +1 -0
  15. package/dist/analyzers/asc-version.d.ts.map +1 -0
  16. package/dist/analyzers/code-scanner.d.ts.map +1 -0
  17. package/dist/analyzers/deprecated-api.d.ts.map +1 -0
  18. package/dist/analyzers/entitlements.d.ts.map +1 -0
  19. package/dist/analyzers/index.d.ts.map +1 -0
  20. package/dist/analyzers/info-plist.d.ts.map +1 -0
  21. package/dist/analyzers/privacy.d.ts.map +1 -0
  22. package/dist/analyzers/private-api.d.ts.map +1 -0
  23. package/dist/analyzers/security.d.ts.map +1 -0
  24. package/dist/analyzers/ui-ux.d.ts.map +1 -0
  25. package/dist/asc/auth.d.ts.map +1 -0
  26. package/dist/asc/client.d.ts.map +1 -0
  27. package/dist/asc/endpoints/apps.d.ts.map +1 -0
  28. package/dist/asc/endpoints/iap.d.ts.map +1 -0
  29. package/dist/asc/endpoints/screenshots.d.ts.map +1 -0
  30. package/dist/asc/endpoints/versions.d.ts.map +1 -0
  31. package/dist/asc/errors.d.ts.map +1 -0
  32. package/dist/asc/index.d.ts.map +1 -0
  33. package/dist/asc/types.d.ts.map +1 -0
  34. package/dist/badge/generator.d.ts.map +1 -0
  35. package/dist/badge/index.d.ts.map +1 -0
  36. package/dist/badge/types.d.ts.map +1 -0
  37. package/dist/cache/file-cache.d.ts.map +1 -0
  38. package/dist/cache/index.d.ts.map +1 -0
  39. package/dist/cache/types.d.ts.map +1 -0
  40. package/dist/cli/commands/help.d.ts.map +1 -0
  41. package/dist/cli/commands/scan.d.ts.map +1 -0
  42. package/dist/cli/commands/version.d.ts.map +1 -0
  43. package/dist/cli/index.d.ts.map +1 -0
  44. package/dist/cli/types.d.ts.map +1 -0
  45. package/dist/git/diff.d.ts.map +1 -0
  46. package/dist/git/index.d.ts.map +1 -0
  47. package/dist/git/types.d.ts.map +1 -0
  48. package/dist/guidelines/database.d.ts.map +1 -0
  49. package/dist/guidelines/index.d.ts.map +1 -0
  50. package/dist/guidelines/matcher.d.ts.map +1 -0
  51. package/dist/guidelines/types.d.ts.map +1 -0
  52. package/dist/history/comparator.d.ts.map +1 -0
  53. package/dist/history/index.d.ts.map +1 -0
  54. package/dist/history/store.d.ts.map +1 -0
  55. package/dist/history/types.d.ts.map +1 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +994 -0
  58. package/dist/parsers/index.d.ts.map +1 -0
  59. package/dist/parsers/plist.d.ts.map +1 -0
  60. package/dist/parsers/xcodeproj.d.ts.map +1 -0
  61. package/dist/progress/index.d.ts.map +1 -0
  62. package/dist/progress/reporter.d.ts.map +1 -0
  63. package/dist/progress/types.d.ts.map +1 -0
  64. package/dist/reports/html.d.ts.map +1 -0
  65. package/dist/reports/index.d.ts.map +1 -0
  66. package/dist/reports/json.d.ts.map +1 -0
  67. package/dist/reports/markdown.d.ts.map +1 -0
  68. package/dist/reports/types.d.ts.map +1 -0
  69. package/dist/rules/engine.d.ts.map +1 -0
  70. package/dist/rules/index.d.ts.map +1 -0
  71. package/dist/rules/loader.d.ts.map +1 -0
  72. package/dist/rules/types.d.ts.map +1 -0
  73. package/dist/types/index.d.ts.map +1 -0
  74. package/docs/ANALYZERS.md +237 -0
  75. package/docs/API.md +308 -0
  76. package/docs/BADGES.md +130 -0
  77. package/docs/CI_CD.md +283 -0
  78. package/docs/CLI.md +140 -0
  79. package/docs/REPORTS.md +212 -0
  80. package/docs/ROADMAP.md +267 -0
  81. package/docs/RULES.md +182 -0
  82. package/docs/SECURITY.md +89 -0
  83. package/docs/TROUBLESHOOTING.md +227 -0
  84. package/docs/tutorials/ASC_SETUP.md +188 -0
  85. package/docs/tutorials/CI_INTEGRATION.md +292 -0
  86. package/docs/tutorials/CUSTOM_RULES.md +291 -0
  87. package/docs/tutorials/GETTING_STARTED.md +226 -0
  88. package/docs/video-scripts/01-introduction.md +106 -0
  89. package/docs/video-scripts/02-cli-usage.md +120 -0
  90. package/docs/video-scripts/03-ci-integration.md +198 -0
  91. package/eslint.config.js +33 -0
  92. package/examples/.ios-review-rules.json +82 -0
  93. package/examples/bitrise-workflow.yml +129 -0
  94. package/examples/fastlane-lane.rb +71 -0
  95. package/examples/github-action.yml +147 -0
  96. package/fastlane/Fastfile.example +114 -0
  97. package/fastlane/README.md +99 -0
  98. package/jest.config.js +36 -0
  99. package/package.json +65 -0
  100. package/scripts/benchmark.ts +112 -0
  101. package/scripts/debug-parser.ts +37 -0
  102. package/scripts/debug-pbxproj.ts +36 -0
  103. package/scripts/debug-specific.ts +47 -0
  104. package/scripts/test-analyze.ts +67 -0
  105. package/scripts/xcode-cloud-review.sh +167 -0
  106. package/src/analyzer.ts +227 -0
  107. package/src/analyzers/asc-iap.ts +300 -0
  108. package/src/analyzers/asc-metadata.ts +326 -0
  109. package/src/analyzers/asc-screenshots.ts +310 -0
  110. package/src/analyzers/asc-version.ts +368 -0
  111. package/src/analyzers/code-scanner.ts +408 -0
  112. package/src/analyzers/deprecated-api.ts +390 -0
  113. package/src/analyzers/entitlements.ts +345 -0
  114. package/src/analyzers/index.ts +12 -0
  115. package/src/analyzers/info-plist.ts +409 -0
  116. package/src/analyzers/privacy.ts +376 -0
  117. package/src/analyzers/private-api.ts +377 -0
  118. package/src/analyzers/security.ts +327 -0
  119. package/src/analyzers/ui-ux.ts +509 -0
  120. package/src/asc/auth.ts +204 -0
  121. package/src/asc/client.ts +258 -0
  122. package/src/asc/endpoints/apps.ts +115 -0
  123. package/src/asc/endpoints/iap.ts +171 -0
  124. package/src/asc/endpoints/screenshots.ts +164 -0
  125. package/src/asc/endpoints/versions.ts +174 -0
  126. package/src/asc/errors.ts +109 -0
  127. package/src/asc/index.ts +108 -0
  128. package/src/asc/types.ts +369 -0
  129. package/src/badge/generator.ts +48 -0
  130. package/src/badge/index.ts +2 -0
  131. package/src/badge/types.ts +5 -0
  132. package/src/cache/file-cache.ts +75 -0
  133. package/src/cache/index.ts +2 -0
  134. package/src/cache/types.ts +10 -0
  135. package/src/cli/commands/help.ts +41 -0
  136. package/src/cli/commands/scan.ts +44 -0
  137. package/src/cli/commands/version.ts +12 -0
  138. package/src/cli/index.ts +92 -0
  139. package/src/cli/types.ts +17 -0
  140. package/src/git/diff.ts +21 -0
  141. package/src/git/index.ts +2 -0
  142. package/src/git/types.ts +5 -0
  143. package/src/guidelines/database.ts +344 -0
  144. package/src/guidelines/index.ts +4 -0
  145. package/src/guidelines/matcher.ts +84 -0
  146. package/src/guidelines/types.ts +28 -0
  147. package/src/history/comparator.ts +114 -0
  148. package/src/history/index.ts +3 -0
  149. package/src/history/store.ts +135 -0
  150. package/src/history/types.ts +40 -0
  151. package/src/index.ts +1113 -0
  152. package/src/parsers/index.ts +3 -0
  153. package/src/parsers/plist.ts +253 -0
  154. package/src/parsers/xcodeproj.ts +265 -0
  155. package/src/progress/index.ts +2 -0
  156. package/src/progress/reporter.ts +65 -0
  157. package/src/progress/types.ts +9 -0
  158. package/src/reports/html.ts +322 -0
  159. package/src/reports/index.ts +20 -0
  160. package/src/reports/json.ts +92 -0
  161. package/src/reports/markdown.ts +187 -0
  162. package/src/reports/types.ts +26 -0
  163. package/src/rules/engine.ts +121 -0
  164. package/src/rules/index.ts +3 -0
  165. package/src/rules/loader.ts +83 -0
  166. package/src/rules/types.ts +25 -0
  167. package/src/types/index.ts +247 -0
  168. package/tests/analyzer.test.ts +142 -0
  169. package/tests/analyzers/asc-iap.test.ts +228 -0
  170. package/tests/analyzers/asc-metadata.test.ts +210 -0
  171. package/tests/analyzers/asc-screenshots.test.ts +135 -0
  172. package/tests/analyzers/asc-version.test.ts +259 -0
  173. package/tests/analyzers/code-scanner.test.ts +745 -0
  174. package/tests/analyzers/deprecated-api.test.ts +286 -0
  175. package/tests/analyzers/entitlements.test.ts +411 -0
  176. package/tests/analyzers/info-plist.test.ts +148 -0
  177. package/tests/analyzers/privacy.test.ts +623 -0
  178. package/tests/analyzers/private-api.test.ts +255 -0
  179. package/tests/analyzers/security.test.ts +300 -0
  180. package/tests/analyzers/ui-ux.test.ts +357 -0
  181. package/tests/asc/auth.test.ts +189 -0
  182. package/tests/asc/client.test.ts +207 -0
  183. package/tests/asc/endpoints.test.ts +1359 -0
  184. package/tests/badge/generator.test.ts +73 -0
  185. package/tests/cache/file-cache.test.ts +124 -0
  186. package/tests/cli/cli-index.test.ts +510 -0
  187. package/tests/cli/commands.test.ts +67 -0
  188. package/tests/cli/scan.test.ts +152 -0
  189. package/tests/git/diff.test.ts +69 -0
  190. package/tests/guidelines/matcher.test.ts +209 -0
  191. package/tests/history/comparator.test.ts +272 -0
  192. package/tests/history/store.test.ts +200 -0
  193. package/tests/integration/cli.test.ts +95 -0
  194. package/tests/integration/e2e.test.ts +130 -0
  195. package/tests/parsers/plist.test.ts +240 -0
  196. package/tests/parsers/xcodeproj.test.ts +289 -0
  197. package/tests/progress/reporter.test.ts +117 -0
  198. package/tests/reports/html.test.ts +176 -0
  199. package/tests/reports/json.test.ts +235 -0
  200. package/tests/reports/markdown.test.ts +196 -0
  201. package/tests/rules/engine.test.ts +229 -0
  202. package/tests/rules/loader.test.ts +187 -0
  203. package/tests/setup.ts +15 -0
  204. package/tsconfig.json +27 -0
  205. package/tsconfig.test.json +9 -0
package/dist/index.js ADDED
@@ -0,0 +1,994 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
5
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
7
+ const analyzer_js_1 = require("./analyzer.js");
8
+ const index_js_2 = require("./types/index.js");
9
+ const index_js_3 = require("./reports/index.js");
10
+ const index_js_4 = require("./history/index.js");
11
+ const index_js_5 = require("./guidelines/index.js");
12
+ const index_js_6 = require("./rules/index.js");
13
+ // Dual-mode: CLI if args provided, MCP server otherwise
14
+ if (process.argv.length > 2) {
15
+ import('./cli/index.js').then(({ runCli }) => runCli(process.argv));
16
+ }
17
+ else {
18
+ const server = new index_js_1.Server({
19
+ name: 'ios-app-review',
20
+ version: '1.0.0',
21
+ }, {
22
+ capabilities: {
23
+ tools: {},
24
+ },
25
+ });
26
+ /**
27
+ * List available tools
28
+ */
29
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
30
+ return {
31
+ tools: [
32
+ {
33
+ name: 'analyze_ios_app',
34
+ description: 'Analyze an iOS app project for App Store review compliance. ' +
35
+ 'Checks Info.plist, privacy manifests, entitlements, and code for common rejection reasons. ' +
36
+ 'Optionally includes App Store Connect validation when credentials are configured.',
37
+ inputSchema: {
38
+ type: 'object',
39
+ properties: {
40
+ projectPath: {
41
+ type: 'string',
42
+ description: 'Path to the .xcodeproj or .xcworkspace directory',
43
+ },
44
+ analyzers: {
45
+ type: 'array',
46
+ items: {
47
+ type: 'string',
48
+ enum: [
49
+ 'all',
50
+ 'info-plist',
51
+ 'privacy',
52
+ 'entitlements',
53
+ 'code',
54
+ 'deprecated-api',
55
+ 'private-api',
56
+ 'security',
57
+ 'ui-ux',
58
+ 'asc-metadata',
59
+ 'asc-screenshots',
60
+ 'asc-version',
61
+ 'asc-iap',
62
+ ],
63
+ },
64
+ description: 'Specific analyzers to run (default: all)',
65
+ },
66
+ targetName: {
67
+ type: 'string',
68
+ description: 'Specific target to analyze (default: main app target)',
69
+ },
70
+ includeASC: {
71
+ type: 'boolean',
72
+ description: 'Include App Store Connect validation (requires ASC_KEY_ID, ASC_ISSUER_ID, and ASC_PRIVATE_KEY_PATH environment variables)',
73
+ },
74
+ bundleId: {
75
+ type: 'string',
76
+ description: 'Bundle ID for ASC validation (auto-detected from project if not provided)',
77
+ },
78
+ },
79
+ required: ['projectPath'],
80
+ },
81
+ },
82
+ {
83
+ name: 'check_info_plist',
84
+ description: 'Validate an Info.plist file for required keys, privacy descriptions, and App Transport Security configuration.',
85
+ inputSchema: {
86
+ type: 'object',
87
+ properties: {
88
+ plistPath: {
89
+ type: 'string',
90
+ description: 'Path to the Info.plist file',
91
+ },
92
+ },
93
+ required: ['plistPath'],
94
+ },
95
+ },
96
+ {
97
+ name: 'check_privacy_manifest',
98
+ description: 'Validate a PrivacyInfo.xcprivacy file for iOS 17+ privacy manifest requirements.',
99
+ inputSchema: {
100
+ type: 'object',
101
+ properties: {
102
+ manifestPath: {
103
+ type: 'string',
104
+ description: 'Path to the PrivacyInfo.xcprivacy file',
105
+ },
106
+ projectPath: {
107
+ type: 'string',
108
+ description: 'Path to the project (for cross-referencing API usage)',
109
+ },
110
+ },
111
+ required: ['manifestPath'],
112
+ },
113
+ },
114
+ {
115
+ name: 'scan_code',
116
+ description: 'Scan Swift/Objective-C code for issues like hardcoded IPs, debug code, secrets, and deprecated APIs.',
117
+ inputSchema: {
118
+ type: 'object',
119
+ properties: {
120
+ path: {
121
+ type: 'string',
122
+ description: 'Path to scan (file or directory)',
123
+ },
124
+ patterns: {
125
+ type: 'array',
126
+ items: { type: 'string' },
127
+ description: 'Specific patterns to check (default: all)',
128
+ },
129
+ },
130
+ required: ['path'],
131
+ },
132
+ },
133
+ {
134
+ name: 'check_deprecated_apis',
135
+ description: 'Scan Swift/Objective-C code for deprecated iOS API usage. ' +
136
+ 'Detects APIs deprecated or removed at your deployment target version.',
137
+ inputSchema: {
138
+ type: 'object',
139
+ properties: {
140
+ path: {
141
+ type: 'string',
142
+ description: 'Path to scan (file or directory)',
143
+ },
144
+ deploymentTarget: {
145
+ type: 'string',
146
+ description: 'iOS deployment target version (e.g. "15.0"). Default: "13.0"',
147
+ },
148
+ },
149
+ required: ['path'],
150
+ },
151
+ },
152
+ {
153
+ name: 'check_private_apis',
154
+ description: 'Scan code for private/undocumented iOS API usage that causes App Store rejection. ' +
155
+ 'Detects private selectors, undocumented frameworks, runtime API access, and private URL schemes.',
156
+ inputSchema: {
157
+ type: 'object',
158
+ properties: {
159
+ path: {
160
+ type: 'string',
161
+ description: 'Path to scan (file or directory)',
162
+ },
163
+ },
164
+ required: ['path'],
165
+ },
166
+ },
167
+ {
168
+ name: 'check_security',
169
+ description: 'Scan code for security vulnerabilities including weak cryptography, insecure data storage, ' +
170
+ 'insecure keychain configuration, SQL injection, and hardcoded secrets.',
171
+ inputSchema: {
172
+ type: 'object',
173
+ properties: {
174
+ path: {
175
+ type: 'string',
176
+ description: 'Path to scan (file or directory)',
177
+ },
178
+ },
179
+ required: ['path'],
180
+ },
181
+ },
182
+ {
183
+ name: 'check_ui_ux',
184
+ description: 'Check UI/UX compliance including launch screen, app icons, iPad support, ' +
185
+ 'placeholder text in storyboards, and accessibility basics.',
186
+ inputSchema: {
187
+ type: 'object',
188
+ properties: {
189
+ projectPath: {
190
+ type: 'string',
191
+ description: 'Path to the project directory or .xcodeproj',
192
+ },
193
+ },
194
+ required: ['projectPath'],
195
+ },
196
+ },
197
+ {
198
+ name: 'validate_asc_metadata',
199
+ description: 'Validate app metadata in App Store Connect including name, subtitle, description, keywords, ' +
200
+ 'privacy policy URL, and support URL. Requires ASC credentials.',
201
+ inputSchema: {
202
+ type: 'object',
203
+ properties: {
204
+ bundleId: {
205
+ type: 'string',
206
+ description: 'Bundle identifier of the app to validate',
207
+ },
208
+ },
209
+ required: ['bundleId'],
210
+ },
211
+ },
212
+ {
213
+ name: 'validate_asc_screenshots',
214
+ description: 'Validate app screenshots in App Store Connect including required device sizes, screenshot counts, ' +
215
+ 'and processing status. Requires ASC credentials.',
216
+ inputSchema: {
217
+ type: 'object',
218
+ properties: {
219
+ bundleId: {
220
+ type: 'string',
221
+ description: 'Bundle identifier of the app to validate',
222
+ },
223
+ },
224
+ required: ['bundleId'],
225
+ },
226
+ },
227
+ {
228
+ name: 'compare_versions',
229
+ description: 'Compare local app version with App Store Connect version. Checks version numbers, build numbers, ' +
230
+ 'submission status, and release notes. Requires ASC credentials.',
231
+ inputSchema: {
232
+ type: 'object',
233
+ properties: {
234
+ bundleId: {
235
+ type: 'string',
236
+ description: 'Bundle identifier of the app',
237
+ },
238
+ localVersion: {
239
+ type: 'string',
240
+ description: 'Local version string (e.g. "1.2.0")',
241
+ },
242
+ localBuild: {
243
+ type: 'string',
244
+ description: 'Local build number (e.g. "42")',
245
+ },
246
+ },
247
+ required: ['bundleId'],
248
+ },
249
+ },
250
+ {
251
+ name: 'validate_iap',
252
+ description: 'Validate in-app purchases in App Store Connect including localizations, review screenshots, ' +
253
+ 'and submission readiness. Requires ASC credentials.',
254
+ inputSchema: {
255
+ type: 'object',
256
+ properties: {
257
+ bundleId: {
258
+ type: 'string',
259
+ description: 'Bundle identifier of the app',
260
+ },
261
+ },
262
+ required: ['bundleId'],
263
+ },
264
+ },
265
+ {
266
+ name: 'full_asc_validation',
267
+ description: 'Run all App Store Connect validations: metadata, screenshots, versions, and in-app purchases. ' +
268
+ 'Requires ASC credentials (ASC_KEY_ID, ASC_ISSUER_ID, ASC_PRIVATE_KEY_PATH).',
269
+ inputSchema: {
270
+ type: 'object',
271
+ properties: {
272
+ bundleId: {
273
+ type: 'string',
274
+ description: 'Bundle identifier of the app',
275
+ },
276
+ },
277
+ required: ['bundleId'],
278
+ },
279
+ },
280
+ {
281
+ name: 'generate_report',
282
+ description: 'Run full analysis and generate an enriched report with review readiness score, ' +
283
+ 'guideline cross-references, and optional historical comparison. Supports markdown, HTML, and JSON output.',
284
+ inputSchema: {
285
+ type: 'object',
286
+ properties: {
287
+ projectPath: {
288
+ type: 'string',
289
+ description: 'Path to the .xcodeproj or .xcworkspace directory',
290
+ },
291
+ format: {
292
+ type: 'string',
293
+ enum: ['markdown', 'html', 'json'],
294
+ description: 'Report output format (default: markdown)',
295
+ },
296
+ includeHistory: {
297
+ type: 'boolean',
298
+ description: 'Include comparison with the most recent previous scan',
299
+ },
300
+ saveToHistory: {
301
+ type: 'boolean',
302
+ description: 'Save this scan to history for future comparisons',
303
+ },
304
+ },
305
+ required: ['projectPath'],
306
+ },
307
+ },
308
+ {
309
+ name: 'compare_scans',
310
+ description: 'Compare the current scan with a previous scan to see new, resolved, and ongoing issues.',
311
+ inputSchema: {
312
+ type: 'object',
313
+ properties: {
314
+ projectPath: {
315
+ type: 'string',
316
+ description: 'Path to the .xcodeproj or .xcworkspace directory',
317
+ },
318
+ previousScanId: {
319
+ type: 'string',
320
+ description: 'ID of a specific previous scan to compare against (default: latest)',
321
+ },
322
+ },
323
+ required: ['projectPath'],
324
+ },
325
+ },
326
+ {
327
+ name: 'view_scan_history',
328
+ description: 'List past scan records with scores and trend analysis for a project.',
329
+ inputSchema: {
330
+ type: 'object',
331
+ properties: {
332
+ projectPath: {
333
+ type: 'string',
334
+ description: 'Path to the project directory',
335
+ },
336
+ limit: {
337
+ type: 'number',
338
+ description: 'Maximum number of scans to return (default: 10)',
339
+ },
340
+ },
341
+ required: ['projectPath'],
342
+ },
343
+ },
344
+ {
345
+ name: 'lookup_guideline',
346
+ description: 'Look up an Apple App Store Review Guideline by section number. Returns the title, excerpt, and URL.',
347
+ inputSchema: {
348
+ type: 'object',
349
+ properties: {
350
+ section: {
351
+ type: 'string',
352
+ description: 'Guideline section number (e.g., "2.5.1", "5.1.1")',
353
+ },
354
+ },
355
+ required: ['section'],
356
+ },
357
+ },
358
+ {
359
+ name: 'validate_custom_rules',
360
+ description: 'Validate and preview a custom rules configuration file (.ios-review-rules.json).',
361
+ inputSchema: {
362
+ type: 'object',
363
+ properties: {
364
+ projectPath: {
365
+ type: 'string',
366
+ description: 'Path to the project directory',
367
+ },
368
+ configPath: {
369
+ type: 'string',
370
+ description: 'Explicit path to the rules config file (default: auto-discover)',
371
+ },
372
+ },
373
+ required: ['projectPath'],
374
+ },
375
+ },
376
+ ],
377
+ };
378
+ });
379
+ /**
380
+ * Handle tool calls
381
+ */
382
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
383
+ const { name, arguments: args } = request.params;
384
+ try {
385
+ switch (name) {
386
+ case 'analyze_ios_app': {
387
+ const input = index_js_2.AnalyzeInputSchema.parse(args);
388
+ const report = await (0, analyzer_js_1.runAnalysis)(input);
389
+ const formatter = (0, index_js_3.createFormatter)('markdown');
390
+ return {
391
+ content: [
392
+ {
393
+ type: 'text',
394
+ text: formatter.format(report),
395
+ },
396
+ ],
397
+ };
398
+ }
399
+ case 'check_info_plist': {
400
+ const { plistPath } = args;
401
+ const { InfoPlistAnalyzer } = await import('./analyzers/info-plist.js');
402
+ const analyzer = new InfoPlistAnalyzer();
403
+ const result = await analyzer.analyzePlist(plistPath);
404
+ return {
405
+ content: [
406
+ {
407
+ type: 'text',
408
+ text: formatAnalysisResult(result),
409
+ },
410
+ ],
411
+ };
412
+ }
413
+ case 'check_privacy_manifest': {
414
+ const { manifestPath, projectPath } = args;
415
+ const { PrivacyAnalyzer } = await import('./analyzers/privacy.js');
416
+ const analyzer = new PrivacyAnalyzer();
417
+ const result = await analyzer.analyzeManifest(manifestPath, projectPath);
418
+ return {
419
+ content: [
420
+ {
421
+ type: 'text',
422
+ text: formatAnalysisResult(result),
423
+ },
424
+ ],
425
+ };
426
+ }
427
+ case 'scan_code': {
428
+ const { path, patterns } = args;
429
+ const { CodeScanner } = await import('./analyzers/code-scanner.js');
430
+ const scanner = new CodeScanner();
431
+ const result = await scanner.scanPath(path, patterns);
432
+ return {
433
+ content: [
434
+ {
435
+ type: 'text',
436
+ text: formatAnalysisResult(result),
437
+ },
438
+ ],
439
+ };
440
+ }
441
+ case 'check_deprecated_apis': {
442
+ const { path: scanPath, deploymentTarget } = args;
443
+ const { DeprecatedAPIAnalyzer } = await import('./analyzers/deprecated-api.js');
444
+ const depAnalyzer = new DeprecatedAPIAnalyzer();
445
+ const result = await depAnalyzer.scanPath(scanPath, deploymentTarget);
446
+ return {
447
+ content: [
448
+ {
449
+ type: 'text',
450
+ text: formatAnalysisResult(result),
451
+ },
452
+ ],
453
+ };
454
+ }
455
+ case 'check_private_apis': {
456
+ const { path: scanPath } = args;
457
+ const { PrivateAPIAnalyzer } = await import('./analyzers/private-api.js');
458
+ const privAnalyzer = new PrivateAPIAnalyzer();
459
+ const result = await privAnalyzer.scanPath(scanPath);
460
+ return {
461
+ content: [
462
+ {
463
+ type: 'text',
464
+ text: formatAnalysisResult(result),
465
+ },
466
+ ],
467
+ };
468
+ }
469
+ case 'check_security': {
470
+ const { path: scanPath } = args;
471
+ const { SecurityAnalyzer } = await import('./analyzers/security.js');
472
+ const secAnalyzer = new SecurityAnalyzer();
473
+ const result = await secAnalyzer.scanPath(scanPath);
474
+ return {
475
+ content: [
476
+ {
477
+ type: 'text',
478
+ text: formatAnalysisResult(result),
479
+ },
480
+ ],
481
+ };
482
+ }
483
+ case 'check_ui_ux': {
484
+ const { projectPath } = args;
485
+ const { UIUXAnalyzer } = await import('./analyzers/ui-ux.js');
486
+ const uiuxAnalyzer = new UIUXAnalyzer();
487
+ const result = await uiuxAnalyzer.validateProject(projectPath);
488
+ return {
489
+ content: [
490
+ {
491
+ type: 'text',
492
+ text: formatAnalysisResult(result),
493
+ },
494
+ ],
495
+ };
496
+ }
497
+ case 'validate_asc_metadata': {
498
+ const { bundleId } = args;
499
+ const { ASCMetadataAnalyzer } = await import('./analyzers/asc-metadata.js');
500
+ const analyzer = new ASCMetadataAnalyzer();
501
+ const result = await analyzer.validateByBundleId(bundleId);
502
+ return {
503
+ content: [
504
+ {
505
+ type: 'text',
506
+ text: formatAnalysisResult(result),
507
+ },
508
+ ],
509
+ };
510
+ }
511
+ case 'validate_asc_screenshots': {
512
+ const { bundleId } = args;
513
+ const { ASCScreenshotAnalyzer } = await import('./analyzers/asc-screenshots.js');
514
+ const analyzer = new ASCScreenshotAnalyzer();
515
+ const result = await analyzer.validateByBundleId(bundleId);
516
+ return {
517
+ content: [
518
+ {
519
+ type: 'text',
520
+ text: formatAnalysisResult(result),
521
+ },
522
+ ],
523
+ };
524
+ }
525
+ case 'compare_versions': {
526
+ const { bundleId, localVersion, localBuild } = args;
527
+ const { ASCVersionAnalyzer } = await import('./analyzers/asc-version.js');
528
+ const analyzer = new ASCVersionAnalyzer();
529
+ const result = await analyzer.compareVersions(bundleId, localVersion, localBuild);
530
+ return {
531
+ content: [
532
+ {
533
+ type: 'text',
534
+ text: formatAnalysisResult(result),
535
+ },
536
+ ],
537
+ };
538
+ }
539
+ case 'validate_iap': {
540
+ const { bundleId } = args;
541
+ const { ASCIAPAnalyzer } = await import('./analyzers/asc-iap.js');
542
+ const analyzer = new ASCIAPAnalyzer();
543
+ const result = await analyzer.validateByBundleId(bundleId);
544
+ return {
545
+ content: [
546
+ {
547
+ type: 'text',
548
+ text: formatAnalysisResult(result),
549
+ },
550
+ ],
551
+ };
552
+ }
553
+ case 'full_asc_validation': {
554
+ const { bundleId } = args;
555
+ const results = await runFullASCValidation(bundleId);
556
+ const summary = calculateSummary(results);
557
+ return {
558
+ content: [
559
+ {
560
+ type: 'text',
561
+ text: formatASCReport(bundleId, results, summary),
562
+ },
563
+ ],
564
+ };
565
+ }
566
+ case 'generate_report': {
567
+ const { projectPath, format, includeHistory, saveToHistory } = args;
568
+ const input = index_js_2.AnalyzeInputSchema.parse({ projectPath });
569
+ const report = await (0, analyzer_js_1.runAnalysis)(input);
570
+ const enriched = { ...report };
571
+ const pathMod = await import('path');
572
+ const basePath = pathMod.dirname(pathMod.resolve(projectPath));
573
+ const store = new index_js_4.HistoryStore(basePath);
574
+ if (includeHistory) {
575
+ const previousScan = await store.getLatestScan();
576
+ if (previousScan) {
577
+ const comparator = new index_js_4.ScanComparator();
578
+ const currentScanRecord = {
579
+ id: 'current',
580
+ timestamp: report.timestamp,
581
+ projectPath: report.projectPath,
582
+ report,
583
+ score: report.score,
584
+ };
585
+ const comparison = comparator.compare(previousScan, currentScanRecord);
586
+ enriched.comparison = {
587
+ previousScanId: previousScan.id,
588
+ previousTimestamp: previousScan.timestamp,
589
+ previousScore: previousScan.score,
590
+ currentScore: report.score,
591
+ scoreDelta: comparison.scoreDelta,
592
+ newIssues: comparison.newIssues.map((fp) => ({
593
+ id: fp,
594
+ title: fp,
595
+ description: '',
596
+ severity: 'info',
597
+ category: 'code',
598
+ })),
599
+ resolvedIssues: comparison.resolvedIssues.map((fp) => ({
600
+ id: fp,
601
+ title: fp,
602
+ description: '',
603
+ severity: 'info',
604
+ category: 'code',
605
+ })),
606
+ ongoingIssues: comparison.ongoingIssues.map((fp) => ({
607
+ id: fp,
608
+ title: fp,
609
+ description: '',
610
+ severity: 'info',
611
+ category: 'code',
612
+ })),
613
+ trend: comparison.trend,
614
+ };
615
+ }
616
+ }
617
+ if (saveToHistory) {
618
+ await store.saveScan(report, report.score);
619
+ }
620
+ const reportFormat = (format === 'html' || format === 'json') ? format : 'markdown';
621
+ const formatter = (0, index_js_3.createFormatter)(reportFormat);
622
+ return {
623
+ content: [
624
+ {
625
+ type: 'text',
626
+ text: formatter.format(enriched),
627
+ },
628
+ ],
629
+ };
630
+ }
631
+ case 'compare_scans': {
632
+ const { projectPath, previousScanId } = args;
633
+ const pathMod = await import('path');
634
+ const resolvedPath = pathMod.resolve(projectPath);
635
+ const basePath = pathMod.dirname(resolvedPath);
636
+ const store = new index_js_4.HistoryStore(basePath);
637
+ const previousScan = previousScanId
638
+ ? await store.getScan(previousScanId)
639
+ : await store.getLatestScan();
640
+ if (!previousScan) {
641
+ return {
642
+ content: [
643
+ {
644
+ type: 'text',
645
+ text: 'No previous scan found. Run `generate_report` with `saveToHistory: true` first.',
646
+ },
647
+ ],
648
+ };
649
+ }
650
+ const input = index_js_2.AnalyzeInputSchema.parse({ projectPath });
651
+ const report = await (0, analyzer_js_1.runAnalysis)(input);
652
+ const comparator = new index_js_4.ScanComparator();
653
+ const currentScanRecord = {
654
+ id: 'current',
655
+ timestamp: report.timestamp,
656
+ projectPath: report.projectPath,
657
+ report,
658
+ score: report.score,
659
+ };
660
+ const comparison = comparator.compare(previousScan, currentScanRecord);
661
+ const lines = [
662
+ '# Scan Comparison',
663
+ '',
664
+ `**Previous scan:** ${previousScan.id} (${previousScan.timestamp})`,
665
+ `**Previous score:** ${previousScan.score}/100`,
666
+ `**Current score:** ${report.score}/100`,
667
+ `**Delta:** ${comparison.scoreDelta > 0 ? '+' : ''}${comparison.scoreDelta}`,
668
+ `**Trend:** ${comparison.trend}`,
669
+ '',
670
+ `| Metric | Count |`,
671
+ `|--------|-------|`,
672
+ `| New issues | ${comparison.newIssues.length} |`,
673
+ `| Resolved issues | ${comparison.resolvedIssues.length} |`,
674
+ `| Ongoing issues | ${comparison.ongoingIssues.length} |`,
675
+ ];
676
+ if (comparison.newIssues.length > 0) {
677
+ lines.push('', '## New Issues', '');
678
+ for (const fp of comparison.newIssues) {
679
+ lines.push(`- ${fp}`);
680
+ }
681
+ }
682
+ if (comparison.resolvedIssues.length > 0) {
683
+ lines.push('', '## Resolved Issues', '');
684
+ for (const fp of comparison.resolvedIssues) {
685
+ lines.push(`- ${fp}`);
686
+ }
687
+ }
688
+ return {
689
+ content: [
690
+ {
691
+ type: 'text',
692
+ text: lines.join('\n'),
693
+ },
694
+ ],
695
+ };
696
+ }
697
+ case 'view_scan_history': {
698
+ const { projectPath, limit } = args;
699
+ const pathMod = await import('path');
700
+ const resolvedPath = pathMod.resolve(projectPath);
701
+ const basePath = pathMod.dirname(resolvedPath);
702
+ const store = new index_js_4.HistoryStore(basePath);
703
+ const scans = await store.listScans(limit ?? 10);
704
+ if (scans.length === 0) {
705
+ return {
706
+ content: [
707
+ {
708
+ type: 'text',
709
+ text: 'No scan history found. Run `generate_report` with `saveToHistory: true` to start tracking.',
710
+ },
711
+ ],
712
+ };
713
+ }
714
+ const lines = [
715
+ '# Scan History',
716
+ '',
717
+ `| # | Date | Score | Git Branch | Git Commit |`,
718
+ `|---|------|-------|------------|------------|`,
719
+ ];
720
+ for (let i = 0; i < scans.length; i++) {
721
+ const scan = scans[i];
722
+ const date = new Date(scan.timestamp).toLocaleString();
723
+ lines.push(`| ${i + 1} | ${date} | ${scan.score}/100 | ${scan.gitBranch ?? '-'} | ${scan.gitCommit ? scan.gitCommit.substring(0, 7) : '-'} |`);
724
+ }
725
+ if (scans.length >= 2) {
726
+ const scores = scans.map((s) => s.score).reverse();
727
+ const first = scores[0];
728
+ const last = scores[scores.length - 1];
729
+ const delta = last - first;
730
+ const trend = delta > 5 ? 'Improving' : delta < -5 ? 'Declining' : 'Stable';
731
+ lines.push('', `**Trend:** ${trend} (${delta > 0 ? '+' : ''}${delta} over ${scans.length} scans)`);
732
+ }
733
+ return {
734
+ content: [
735
+ {
736
+ type: 'text',
737
+ text: lines.join('\n'),
738
+ },
739
+ ],
740
+ };
741
+ }
742
+ case 'lookup_guideline': {
743
+ const { section } = args;
744
+ const guideline = index_js_5.GUIDELINES[section];
745
+ if (!guideline) {
746
+ const available = Object.keys(index_js_5.GUIDELINES).sort().join(', ');
747
+ return {
748
+ content: [
749
+ {
750
+ type: 'text',
751
+ text: `Guideline section "${section}" not found.\n\nAvailable sections: ${available}`,
752
+ },
753
+ ],
754
+ };
755
+ }
756
+ return {
757
+ content: [
758
+ {
759
+ type: 'text',
760
+ text: [
761
+ `# Guideline ${guideline.section}: ${guideline.title}`,
762
+ '',
763
+ `**Category:** ${guideline.category}`,
764
+ `**Severity Weight:** ${guideline.severityWeight}/10`,
765
+ '',
766
+ guideline.excerpt,
767
+ '',
768
+ `**Reference:** ${guideline.url}`,
769
+ ].join('\n'),
770
+ },
771
+ ],
772
+ };
773
+ }
774
+ case 'validate_custom_rules': {
775
+ const { projectPath, configPath } = args;
776
+ const loader = new index_js_6.RuleLoader();
777
+ const pathMod = await import('path');
778
+ const resolvedPath = pathMod.resolve(projectPath);
779
+ const basePath = pathMod.dirname(resolvedPath);
780
+ const foundPath = configPath ?? await loader.findConfig(basePath);
781
+ if (!foundPath) {
782
+ return {
783
+ content: [
784
+ {
785
+ type: 'text',
786
+ text: 'No `.ios-review-rules.json` found in the project directory hierarchy.',
787
+ },
788
+ ],
789
+ };
790
+ }
791
+ try {
792
+ const config = await loader.loadConfig(foundPath);
793
+ const compiled = loader.compileRules(config);
794
+ const lines = [
795
+ '# Custom Rules Validation',
796
+ '',
797
+ `**Config file:** ${foundPath}`,
798
+ `**Version:** ${config.version}`,
799
+ `**Rules:** ${config.rules.length}`,
800
+ `**Disabled rules:** ${config.disabledRules?.length ?? 0}`,
801
+ `**Severity overrides:** ${config.severityOverrides ? Object.keys(config.severityOverrides).length : 0}`,
802
+ '',
803
+ '## Rules',
804
+ '',
805
+ '| ID | Title | Severity | Pattern | File Types |',
806
+ '|----|-------|----------|---------|------------|',
807
+ ];
808
+ for (const rule of compiled) {
809
+ const fileTypes = rule.fileTypes?.join(', ') ?? 'all';
810
+ lines.push(`| ${rule.id} | ${rule.title} | ${rule.severity} | \`${rule.pattern}\` | ${fileTypes} |`);
811
+ }
812
+ if (config.disabledRules && config.disabledRules.length > 0) {
813
+ lines.push('', '## Disabled Rules', '');
814
+ for (const id of config.disabledRules) {
815
+ lines.push(`- ${id}`);
816
+ }
817
+ }
818
+ lines.push('', 'Validation: PASSED');
819
+ return {
820
+ content: [
821
+ {
822
+ type: 'text',
823
+ text: lines.join('\n'),
824
+ },
825
+ ],
826
+ };
827
+ }
828
+ catch (error) {
829
+ const message = error instanceof Error ? error.message : String(error);
830
+ return {
831
+ content: [
832
+ {
833
+ type: 'text',
834
+ text: `Custom rules validation FAILED:\n\n${message}`,
835
+ },
836
+ ],
837
+ isError: true,
838
+ };
839
+ }
840
+ }
841
+ default:
842
+ return {
843
+ content: [
844
+ {
845
+ type: 'text',
846
+ text: `Unknown tool: ${name}`,
847
+ },
848
+ ],
849
+ isError: true,
850
+ };
851
+ }
852
+ }
853
+ catch (error) {
854
+ const message = error instanceof Error ? error.message : String(error);
855
+ return {
856
+ content: [
857
+ {
858
+ type: 'text',
859
+ text: `Error: ${message}`,
860
+ },
861
+ ],
862
+ isError: true,
863
+ };
864
+ }
865
+ });
866
+ async function runFullASCValidation(bundleId) {
867
+ const { ASCMetadataAnalyzer } = await import('./analyzers/asc-metadata.js');
868
+ const { ASCScreenshotAnalyzer } = await import('./analyzers/asc-screenshots.js');
869
+ const { ASCVersionAnalyzer } = await import('./analyzers/asc-version.js');
870
+ const { ASCIAPAnalyzer } = await import('./analyzers/asc-iap.js');
871
+ const results = await Promise.all([
872
+ new ASCMetadataAnalyzer().validateByBundleId(bundleId),
873
+ new ASCScreenshotAnalyzer().validateByBundleId(bundleId),
874
+ new ASCVersionAnalyzer().compareVersions(bundleId),
875
+ new ASCIAPAnalyzer().validateByBundleId(bundleId),
876
+ ]);
877
+ return results;
878
+ }
879
+ function calculateSummary(results) {
880
+ let errors = 0;
881
+ let warnings = 0;
882
+ let info = 0;
883
+ let totalDuration = 0;
884
+ for (const result of results) {
885
+ totalDuration += result.duration;
886
+ for (const issue of result.issues) {
887
+ switch (issue.severity) {
888
+ case 'error':
889
+ errors++;
890
+ break;
891
+ case 'warning':
892
+ warnings++;
893
+ break;
894
+ case 'info':
895
+ info++;
896
+ break;
897
+ }
898
+ }
899
+ }
900
+ return {
901
+ totalIssues: errors + warnings + info,
902
+ errors,
903
+ warnings,
904
+ info,
905
+ passed: errors === 0,
906
+ duration: totalDuration,
907
+ };
908
+ }
909
+ function formatAnalysisResult(result) {
910
+ const lines = [
911
+ `# ${result.analyzer} Analysis`,
912
+ '',
913
+ `**Status:** ${result.passed ? 'PASSED' : 'ISSUES FOUND'}`,
914
+ `**Duration:** ${result.duration}ms`,
915
+ '',
916
+ ];
917
+ if (result.issues.length === 0) {
918
+ lines.push('No issues found.');
919
+ }
920
+ else {
921
+ lines.push(`## Issues (${result.issues.length})`);
922
+ lines.push('');
923
+ for (const issue of result.issues) {
924
+ const icon = issue.severity === 'error' ? '[ERROR]' : issue.severity === 'warning' ? '[WARN]' : '[INFO]';
925
+ lines.push(`### ${icon} ${issue.title}`);
926
+ lines.push('');
927
+ lines.push(issue.description);
928
+ if (issue.filePath) {
929
+ const location = issue.lineNumber
930
+ ? `${issue.filePath}:${issue.lineNumber}`
931
+ : issue.filePath;
932
+ lines.push(`\n**Location:** \`${location}\``);
933
+ }
934
+ if (issue.suggestion) {
935
+ lines.push(`\n**Suggestion:** ${issue.suggestion}`);
936
+ }
937
+ lines.push('');
938
+ }
939
+ }
940
+ return lines.join('\n');
941
+ }
942
+ function formatASCReport(bundleId, results, summary) {
943
+ const lines = [
944
+ '# App Store Connect Validation Report',
945
+ '',
946
+ `**Bundle ID:** ${bundleId}`,
947
+ `**Date:** ${new Date().toISOString()}`,
948
+ `**Status:** ${summary.passed ? 'PASSED' : 'ISSUES FOUND'}`,
949
+ '',
950
+ '## Summary',
951
+ '',
952
+ `- **Total Issues:** ${summary.totalIssues}`,
953
+ `- **Errors:** ${summary.errors}`,
954
+ `- **Warnings:** ${summary.warnings}`,
955
+ `- **Info:** ${summary.info}`,
956
+ `- **Duration:** ${summary.duration}ms`,
957
+ '',
958
+ ];
959
+ for (const result of results) {
960
+ lines.push(`## ${result.analyzer}`);
961
+ lines.push('');
962
+ if (result.issues.length === 0) {
963
+ lines.push('No issues found');
964
+ }
965
+ else {
966
+ for (const issue of result.issues) {
967
+ const icon = issue.severity === 'error' ? '[ERROR]' : issue.severity === 'warning' ? '[WARN]' : '[INFO]';
968
+ lines.push(`### ${icon} ${issue.title}`);
969
+ lines.push('');
970
+ lines.push(issue.description);
971
+ if (issue.guideline) {
972
+ lines.push(`\n**Guideline:** ${issue.guideline}`);
973
+ }
974
+ if (issue.suggestion) {
975
+ lines.push(`\n**Suggestion:** ${issue.suggestion}`);
976
+ }
977
+ lines.push('');
978
+ }
979
+ }
980
+ lines.push('');
981
+ }
982
+ return lines.join('\n');
983
+ }
984
+ async function main() {
985
+ const transport = new stdio_js_1.StdioServerTransport();
986
+ await server.connect(transport);
987
+ console.error('iOS App Review MCP server started');
988
+ }
989
+ main().catch((error) => {
990
+ console.error('Fatal error:', error);
991
+ process.exit(1);
992
+ });
993
+ } // end MCP server mode
994
+ //# sourceMappingURL=index.js.map