Skip to content

Commit 3080161

Browse files
authored
Merge pull request #114 from ProvarTesting/fix/mcp-metadata-download-ux
fix(mcp): improve metadata download UX and secrets field descriptions
2 parents 22f397d + 728c0dd commit 3080161

6 files changed

Lines changed: 143 additions & 8 deletions

File tree

.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,7 @@ mochawesome-report
5656
.env.*.local
5757

5858
# Claude
59-
.claude/skills
60-
CLAUDE.md
59+
.claude/
6160

6261
# NitroX schema files — do not commit until IP/licensing confirmed with Provar team
6362
# See: src/mcp/tools/nitroXTools.ts and plan notes

CLAUDE.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Claude Code — Project Instructions
2+
3+
This file is read automatically by Claude Code at the start of every session. Follow these rules when working in this repo.
4+
5+
---
6+
7+
## Documentation Requirements
8+
9+
**Every PR that adds, modifies, or removes an MCP tool must update documentation.** The full set of places to update:
10+
11+
| What changed | Where to update |
12+
|---|---|
13+
| New MCP tool | `docs/mcp.md` (add tool entry with schema + example), `docs/mcp-pilot-guide.md` (add evaluation scenario if user-facing), `README.md` (update tool count if referenced) |
14+
| Modified tool description / parameters / errors | `docs/mcp.md` (update the relevant tool section) |
15+
| Removed tool | `docs/mcp.md`, `docs/mcp-pilot-guide.md`, `README.md` |
16+
| New error code or suggestion field | `docs/mcp.md` (Troubleshooting or Error Codes section) |
17+
| Security model change (path policy, licence, transport) | `docs/mcp.md` (Security Model section), `docs/mcp-pilot-guide.md` (Security Model section) |
18+
| New NitroX tool or schema rule | `docs/mcp.md` (NitroX section), `docs/mcp-pilot-guide.md` (Scenario 7) |
19+
| Smoke test count change | Update `TOTAL_EXPECTED` in `scripts/mcp-smoke.cjs` |
20+
21+
**External / customer-facing docs** (`docs/provar-mcp-public-docs.md`, `docs/university-of-provar-mcp-course.md`) are maintained separately by the Provar team — flag changes that affect public-facing behaviour in your PR description so those can be updated manually.
22+
23+
---
24+
25+
## Test Coverage Requirements
26+
27+
Every PR must include tests for new or changed behaviour:
28+
29+
- **New MCP tool** → unit tests in `test/unit/mcp/<toolGroup>Tools.test.ts` covering: happy path, path policy rejection, missing required fields, and any tool-specific error codes
30+
- **Modified error handling** → at least one positive test (error path fires) and one negative test (does not fire for non-matching input)
31+
- **New validation rule** → test that the rule fires correctly and that a valid input passes
32+
- **Smoke test** → add an entry in `scripts/mcp-smoke.cjs` for each new tool; update `TOTAL_EXPECTED`
33+
34+
Run before every commit:
35+
```sh
36+
yarn test:only # unit tests — must all pass
37+
node scripts/mcp-smoke.cjs 2>/dev/null # smoke — must show all PASS
38+
yarn compile # TypeScript — must be clean
39+
```
40+
41+
---
42+
43+
## MCP Tool Authoring Standards
44+
45+
### Tool description quality
46+
47+
Every tool description must answer these questions for an AI agent reading it cold:
48+
49+
1. **What does it do?** (one sentence)
50+
2. **What prerequisite tools must run first?** (e.g. `config.load` before `metadata.download`)
51+
3. **What are the correct flags / parameters?** Include a concrete example in the `flags` field description when flags are free-form
52+
4. **What does a failure mean?** If a known error pattern exists (e.g. `[DOWNLOAD_ERROR]` = auth failure), say so in the description or return a `details.suggestion`
53+
54+
### Field descriptions
55+
56+
- Fields that accept a **string key or password** must say "string value, NOT a file path" if there is any risk of confusion with a path
57+
- Fields that accept a **file path** must note if the path must be within `--allowed-paths`
58+
- Optional fields that have a dangerous default (e.g. overwriting existing files) must call that out
59+
60+
### Error responses
61+
62+
- Return `details: { suggestion: '...' }` when a known error pattern maps to a common root cause and there is an actionable fix
63+
- Never pass `details: {}` — omit `details` entirely when there is nothing extra to say (keeps the response shape stable)
64+
- Error codes follow `SCREAMING_SNAKE_CASE`; document new codes in `docs/mcp.md`
65+
66+
### Path safety
67+
68+
- Call `assertPathAllowed(path, config.allowedPaths)` on **every** path input before any file operation — not just the output path
69+
- Use `path.resolve()` before `fs.existsSync` / `fs.readFileSync` / `fs.writeFileSync`
70+
- Never construct shell commands from user input; use `spawnSync` with an args array
71+
72+
---
73+
74+
## Branch and PR Conventions
75+
76+
- Feature branches off `develop`: `feature/<description>`
77+
- Bug/fix branches off `develop`: `fix/<description>`
78+
- Release branches off `develop`: `release-v<semver>`
79+
- PRs target `develop`; releases are merged develop → main
80+
- Version in `package.json` follows `<major>.<minor>.<patch>-beta.<n>` on develop
81+
- Bump the beta suffix (`beta.N → beta.N+1`) on any PR that triggers a publish
82+
83+
---
84+
85+
## Lint
86+
87+
The project uses ESLint with `@typescript-eslint` strict rules. Common gotchas:
88+
89+
- `complexity` max is **20** — extract helpers if a function grows past that
90+
- `no-unsafe-assignment` / `no-unsafe-call` — cast through `unknown` not `any`
91+
- `no-unnecessary-type-assertion` — TypeScript narrows after `typeof x === 'string'` checks; remove the redundant cast
92+
- `camelcase``nitroX` is valid camelCase (capital X starts the next word)
93+
94+
CI runs lint as part of `sf-prepack` — do not skip with `--no-verify` on the final merge commit.

src/mcp/tools/antTools.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ export function registerAntGenerate(server: McpServer, config: ServerConfig): vo
192192
.string()
193193
.default('${env.ProvarSecretsPassword}')
194194
.describe(
195-
'Password for the Provar secrets store. Defaults to reading from the ProvarSecretsPassword environment variable.'
195+
'Encryption key used to decrypt the Provar .secrets file (the password string itself, not a file path). Defaults to reading from the ProvarSecretsPassword environment variable.'
196196
),
197197
test_environment_secrets_password: z
198198
.string()

src/mcp/tools/automationTools.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -233,12 +233,29 @@ export function registerAutomationCompile(server: McpServer): void {
233233

234234
// ── Tool: provar.automation.metadata.download ─────────────────────────────────
235235

236+
const DOWNLOAD_ERROR_SUGGESTION =
237+
'A [DOWNLOAD_ERROR] almost always means a Salesforce authentication failure for the connection being used. ' +
238+
'Check: (1) the connection credentials in the Provar project .secrets file are current and not expired; ' +
239+
'(2) the named connection exists in the project and the name is spelled correctly (case-sensitive); ' +
240+
'(3) if using a scratch org, confirm it has not expired (`sf org list`); ' +
241+
'(4) if testprojectSecrets is set in provardx-properties.json, it must be the encryption key string used to decrypt .secrets — not a file path.';
242+
236243
export function registerAutomationMetadataDownload(server: McpServer): void {
237244
server.tool(
238245
'provar.automation.metadata.download',
239-
'Download Salesforce metadata into a Provar project. Invokes `sf provar automation metadata download`.',
246+
[
247+
'Download Salesforce metadata for one or more connections into a Provar project.',
248+
'Invokes `sf provar automation metadata download`.',
249+
'PREREQUISITE: Call provar.automation.config.load first — without it the command fails with MISSING_FILE.',
250+
'Use the -c flag to specify connections: flags: ["-c", "ConnectionName1,ConnectionName2"].',
251+
'Connection names are case-sensitive and must match the names defined in the Provar project.',
252+
'If the download fails with [DOWNLOAD_ERROR], this is almost always a Salesforce authentication issue —',
253+
'check that the credentials in the project .secrets file are current and that any referenced scratch orgs have not expired.',
254+
].join(' '),
240255
{
241-
flags: z.array(z.string()).optional().default([]).describe('Raw CLI flags to forward (e.g. ["--target-org", "myorg"])'),
256+
flags: z.array(z.string()).optional().default([]).describe(
257+
'Raw CLI flags to forward. Use ["-c", "Name1,Name2"] (or the equivalent --connections form) to target specific connections. Example: ["-c", "MyOrg,SandboxOrg"]'
258+
),
242259
sf_path: z.string().optional().describe('Path to the sf CLI executable when not in PATH (e.g. "~/.nvm/versions/node/v22.0.0/bin/sf")'),
243260
},
244261
({ flags, sf_path }) => {
@@ -247,12 +264,17 @@ export function registerAutomationMetadataDownload(server: McpServer): void {
247264

248265
try {
249266
const result = runSfCommand(['provar', 'automation', 'metadata', 'download', ...flags], sf_path);
250-
const response = { requestId, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
267+
const message = result.stderr || result.stdout;
251268

252269
if (result.exitCode !== 0) {
253-
return { isError: true as const, content: [{ type: 'text' as const, text: JSON.stringify(makeError('AUTOMATION_METADATA_FAILED', result.stderr || result.stdout, requestId)) }] };
270+
const isDownloadError = message.includes('[DOWNLOAD_ERROR]');
271+
const details: Record<string, unknown> | undefined = isDownloadError
272+
? { suggestion: DOWNLOAD_ERROR_SUGGESTION }
273+
: undefined;
274+
return { isError: true as const, content: [{ type: 'text' as const, text: JSON.stringify(makeError('AUTOMATION_METADATA_FAILED', message, requestId, false, details)) }] };
254275
}
255276

277+
const response = { requestId, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
256278
return { content: [{ type: 'text' as const, text: JSON.stringify(response) }], structuredContent: response };
257279
} catch (err) {
258280
return handleSpawnError(err, requestId, 'provar.automation.metadata.download');

src/mcp/tools/propertiesTools.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,10 @@ const updatesSchema = z.object({
224224
pluginOutputlevel: z.enum(['SEVERE', 'WARNING', 'INFO', 'FINE', 'FINER', 'FINEST']).optional().describe('Amount of plugin output logged'),
225225
stopOnError: z.boolean().optional().describe('Abort test run on first failure'),
226226
excludeCallable: z.boolean().optional().describe('Omit callable test cases from execution'),
227-
testprojectSecrets: z.string().optional().describe('Test project secrets encryption password'),
227+
testprojectSecrets: z.string().optional().describe(
228+
'Encryption key (password string) used to decrypt the .secrets file in the Provar project root. ' +
229+
'This is the key itself — NOT a file path. Omit this field unless your project uses secrets encryption.'
230+
),
228231
environment: z.object({
229232
testEnvironment: z.string().optional().describe('Name of the test environment to run against'),
230233
webBrowser: z.enum(['Chrome', 'Safari', 'Edge', 'Edge_Legacy', 'Firefox', 'IE', 'Chrome_Headless']).optional(),

test/unit/mcp/automationTools.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,23 @@ describe('automationTools', () => {
422422
const result = server.call('provar.automation.metadata.download', { flags: [] });
423423
assert.equal(parseBody(result).error_code, 'SF_NOT_FOUND');
424424
});
425+
426+
it('includes suggestion in details when [DOWNLOAD_ERROR] is in the message', () => {
427+
spawnStub.returns(makeSpawnResult('', 'Error (1): [DOWNLOAD_ERROR] ERROR\n', 1));
428+
const result = server.call('provar.automation.metadata.download', { flags: ['-c', 'MyOrg'] });
429+
assert.ok(isError(result));
430+
const body = parseBody(result);
431+
assert.equal(body.error_code, 'AUTOMATION_METADATA_FAILED');
432+
assert.ok(body.details && typeof (body.details as Record<string, unknown>).suggestion === 'string', 'Expected suggestion in details for DOWNLOAD_ERROR');
433+
});
434+
435+
it('does NOT include suggestion for other failure messages', () => {
436+
spawnStub.returns(makeSpawnResult('', 'Error (2): Nonexistent flag: --properties-file\n', 1));
437+
const result = server.call('provar.automation.metadata.download', { flags: [] });
438+
assert.ok(isError(result));
439+
const body = parseBody(result);
440+
assert.ok(!body.details || !(body.details as Record<string, unknown>).suggestion, 'Expected no suggestion for non-DOWNLOAD_ERROR');
441+
});
425442
});
426443

427444
// ── provar.automation.config.load ─────────────────────────────────────────

0 commit comments

Comments
 (0)