Skip to content

Commit 51a0b6a

Browse files
committed
Run MCP smoke tests with Claude Code
1 parent 7db233c commit 51a0b6a

4 files changed

Lines changed: 192 additions & 6 deletions

File tree

.github/scripts/mcp-smoke/README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ This harness powers `.github/workflows/mcp-ai-smoke.yml`.
1919
- Additional MCP tool calls are accepted if the expected tool succeeds at least once.
2020
- Handles empty `cases.json` safely and emits an empty `results.json`.
2121

22-
3. `run-gemini-smoke.sh` / `run-codex-smoke.sh`
22+
3. `run-gemini-smoke.sh` / `run-codex-smoke.sh` / `run-claude-smoke.sh`
2323
- Thin wrappers that select the provider-specific runtime setup before invoking the shared harness.
2424

2525
4. `.github/actions/setup-mcp-smoke-env`
@@ -38,9 +38,11 @@ This harness powers `.github/workflows/mcp-ai-smoke.yml`.
3838

3939
- Gemini uses a temporary `settings.json` (`mcpServers`) generated at runtime outside the uploaded artifact tree and remains configurable via `GEMINI_CLI_CMD` and `GEMINI_EXTRA_ARGS`.
4040
- Codex uses a temporary CI-local home/config outside the uploaded artifact tree and registers the Matomo MCP endpoint there at runtime.
41+
- Claude uses a temporary CI-local home/config outside the uploaded artifact tree and loads the Matomo MCP endpoint via a generated HTTP MCP config file at runtime.
4142
- Provider login state and runtime configuration stay outside uploaded artifacts.
42-
- The workflow can run providers independently based on secret availability (`GEMINI_APIKEY`, `OPENAI_APIKEY`).
43-
- Gemini and Codex run in separate CI jobs with isolated Matomo environments and log files.
43+
- The workflow can run providers independently based on secret availability (`GEMINI_APIKEY`, `OPENAI_APIKEY`, `CLAUDE_APIKEY`).
44+
- Claude defaults to the generic `sonnet` alias, with `claude_model` still available in `workflow_dispatch` for pinned-model overrides.
45+
- Gemini, Codex, and Claude run in separate CI jobs with isolated Matomo environments and log files.
4446
- Provider jobs share a local composite action for the common Matomo/bootstrap sequence.
4547
- The workflow is intentionally report-first/non-blocking for internal prototype usage.
4648
- The workflow uploads one artifact per provider job and builds a combined summary from the downloaded per-provider `results.json` files.

.github/scripts/mcp-smoke/run-ai-smoke.sh

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ mkdir -p \
3636
"$provider_state_dir"
3737

3838
cleanup() {
39-
unset OPENAI_APIKEY MCP_AUTH_TOKEN GEMINI_API_KEY
39+
unset OPENAI_APIKEY MCP_AUTH_TOKEN GEMINI_API_KEY ANTHROPIC_API_KEY
4040

4141
if [ -n "${provider_state_dir:-}" ] && [ -d "$provider_state_dir" ]; then
4242
rm -rf "$provider_state_dir"
@@ -139,6 +139,34 @@ setup_provider() {
139139
--bearer-token-env-var MCP_AUTH_TOKEN \
140140
>/dev/null 2>&1
141141
;;
142+
claude)
143+
ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:?ANTHROPIC_API_KEY is required}
144+
CLAUDE_MODEL=${CLAUDE_MODEL:-sonnet}
145+
CLAUDE_CLI_CMD=${CLAUDE_CLI_CMD:-claude}
146+
CLAUDE_HOME_DIR="$provider_state_dir/home"
147+
CLAUDE_MCP_CONFIG_FILE="$provider_state_dir/mcp.json"
148+
149+
if ! command -v "$CLAUDE_CLI_CMD" >/dev/null 2>&1; then
150+
echo "Claude CLI command not found: $CLAUDE_CLI_CMD" >&2
151+
exit 1
152+
fi
153+
154+
mkdir -p "$CLAUDE_HOME_DIR"
155+
jq -n \
156+
--arg mcp_url "$MCP_URL" \
157+
--arg auth_header "Bearer $TOKEN_AUTH" \
158+
'{
159+
"mcpServers": {
160+
"matomo": {
161+
"type": "http",
162+
"url": $mcp_url,
163+
"headers": {
164+
"Authorization": $auth_header
165+
}
166+
}
167+
}
168+
}' > "$CLAUDE_MCP_CONFIG_FILE"
169+
;;
142170
*)
143171
echo "Unsupported SMOKE_PROVIDER: $SMOKE_PROVIDER" >&2
144172
exit 1
@@ -198,6 +226,29 @@ run_provider() {
198226
> /dev/null 2>&1 < "$prompt_file"
199227
fi
200228
;;
229+
claude)
230+
if [ "$CASE_TIMEOUT_SECONDS" -gt 0 ]; then
231+
ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" HOME="$CLAUDE_HOME_DIR" \
232+
timeout "${CASE_TIMEOUT_SECONDS}s" "$CLAUDE_CLI_CMD" \
233+
--mcp-config "$CLAUDE_MCP_CONFIG_FILE" \
234+
--strict-mcp-config \
235+
--dangerously-skip-permissions \
236+
-p \
237+
--model "$CLAUDE_MODEL" \
238+
"$(cat "$prompt_file")" \
239+
> "$transcript_file" 2>&1 < /dev/null
240+
else
241+
ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" HOME="$CLAUDE_HOME_DIR" \
242+
"$CLAUDE_CLI_CMD" \
243+
--mcp-config "$CLAUDE_MCP_CONFIG_FILE" \
244+
--strict-mcp-config \
245+
--dangerously-skip-permissions \
246+
-p \
247+
--model "$CLAUDE_MODEL" \
248+
"$(cat "$prompt_file")" \
249+
> "$transcript_file" 2>&1 < /dev/null
250+
fi
251+
;;
201252
esac
202253
}
203254

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
export SMOKE_PROVIDER=claude
5+
"$(dirname "$0")/run-ai-smoke.sh"

.github/workflows/mcp-ai-smoke.yml

Lines changed: 130 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ on:
1313
description: Codex model
1414
required: false
1515
default: gpt-5-mini
16+
claude_model:
17+
description: Claude model
18+
required: false
19+
default: sonnet
1620

1721
permissions:
1822
actions: read
@@ -41,6 +45,7 @@ env:
4145
MYSQL_DATABASE: matomo
4246
GEMINI_MODEL: ${{ github.event.inputs.gemini_model || 'gemini-2.5-flash' }}
4347
CODEX_MODEL: ${{ github.event.inputs.codex_model || 'gpt-5-mini' }}
48+
CLAUDE_MODEL: ${{ github.event.inputs.claude_model || 'sonnet' }}
4449

4550
jobs:
4651
preflight:
@@ -50,17 +55,20 @@ jobs:
5055
should_run: ${{ steps.check.outputs.should_run }}
5156
has_gemini: ${{ steps.check.outputs.has_gemini }}
5257
has_codex: ${{ steps.check.outputs.has_codex }}
58+
has_claude: ${{ steps.check.outputs.has_claude }}
5359
reason: ${{ steps.check.outputs.reason }}
5460
steps:
5561
- id: check
5662
name: Check secret availability
5763
env:
5864
GEMINI_APIKEY: ${{ secrets.GEMINI_APIKEY }}
5965
OPENAI_APIKEY: ${{ secrets.OPENAI_APIKEY }}
66+
CLAUDE_APIKEY: ${{ secrets.CLAUDE_APIKEY }}
6067
run: |
6168
set -euo pipefail
6269
has_gemini=false
6370
has_codex=false
71+
has_claude=false
6472
6573
if [ -n "${GEMINI_APIKEY:-}" ]; then
6674
has_gemini=true
@@ -70,10 +78,15 @@ jobs:
7078
has_codex=true
7179
fi
7280
81+
if [ -n "${CLAUDE_APIKEY:-}" ]; then
82+
has_claude=true
83+
fi
84+
7385
echo "has_gemini=${has_gemini}" >> "$GITHUB_OUTPUT"
7486
echo "has_codex=${has_codex}" >> "$GITHUB_OUTPUT"
87+
echo "has_claude=${has_claude}" >> "$GITHUB_OUTPUT"
7588
76-
if [ "$has_gemini" = false ] && [ "$has_codex" = false ]; then
89+
if [ "$has_gemini" = false ] && [ "$has_codex" = false ] && [ "$has_claude" = false ]; then
7790
echo "should_run=false" >> "$GITHUB_OUTPUT"
7891
echo "reason=No provider secret is available in this context (likely fork PR)." >> "$GITHUB_OUTPUT"
7992
else
@@ -89,6 +102,7 @@ jobs:
89102
echo "- should_run: ${{ steps.check.outputs.should_run }}"
90103
echo "- Gemini available: ${{ steps.check.outputs.has_gemini }}"
91104
echo "- Codex available: ${{ steps.check.outputs.has_codex }}"
105+
echo "- Claude available: ${{ steps.check.outputs.has_claude }}"
92106
echo "- reason: ${{ steps.check.outputs.reason }}"
93107
} >> "$GITHUB_STEP_SUMMARY"
94108
@@ -294,10 +308,111 @@ jobs:
294308
path: .github/scripts/mcp-smoke/artifacts/codex
295309
if-no-files-found: warn
296310

311+
smoke_claude:
312+
name: API + Claude smoke
313+
runs-on: ubuntu-24.04
314+
needs: preflight
315+
if: needs.preflight.outputs.has_claude == 'true'
316+
continue-on-error: true
317+
timeout-minutes: 30
318+
services:
319+
mysql:
320+
image: mysql:5.7
321+
env:
322+
MYSQL_ROOT_PASSWORD: root
323+
MYSQL_DATABASE: matomo
324+
ports:
325+
- 3306:3306
326+
options: >-
327+
--health-cmd="mysqladmin ping -h 127.0.0.1 -uroot -proot"
328+
--health-interval=10s
329+
--health-timeout=5s
330+
--health-retries=30
331+
steps:
332+
- uses: actions/checkout@v6
333+
with:
334+
fetch-depth: 1
335+
lfs: false
336+
persist-credentials: false
337+
338+
- name: Setup shared smoke environment
339+
uses: ./.github/actions/setup-mcp-smoke-env
340+
with:
341+
plugin_name: ${{ env.PLUGIN_NAME }}
342+
matomo_dir: ${{ env.MATOMO_DIR }}
343+
base_url: ${{ env.BASE_URL }}
344+
mysql_host: ${{ env.MYSQL_HOST }}
345+
mysql_port: ${{ env.MYSQL_PORT }}
346+
mysql_user: ${{ env.MYSQL_USER }}
347+
mysql_password: ${{ env.MYSQL_PASSWORD }}
348+
mysql_database: ${{ env.MYSQL_DATABASE }}
349+
matomo_test_target: maximum_supported_matomo
350+
351+
- name: Install Claude CLI
352+
run: npm install -g @anthropic-ai/claude-code
353+
354+
- name: Claude MCP smoke suite
355+
run: ./.github/scripts/mcp-smoke/run-claude-smoke.sh
356+
env:
357+
ANTHROPIC_API_KEY: ${{ secrets.CLAUDE_APIKEY }}
358+
CLAUDE_MODEL: ${{ env.CLAUDE_MODEL }}
359+
BASE_URL: ${{ env.BASE_URL }}
360+
STATE_FILE: ${{ github.workspace }}/.github/scripts/mcp-smoke/.state.json
361+
CASES_FILE: ${{ github.workspace }}/.github/scripts/mcp-smoke/cases.json
362+
PROMPTS_DIR: ${{ github.workspace }}/.github/scripts/mcp-smoke/prompts
363+
ARTIFACT_DIR: ${{ github.workspace }}/.github/scripts/mcp-smoke/artifacts
364+
MATOMO_LOG_FILE: ${{ env.MATOMO_DIR }}/tmp/logs/matomo.log
365+
PHP_SERVER_LOG_FILE: ${{ env.MATOMO_DIR }}/tmp/logs/php-server.log
366+
367+
- name: Build Claude summary
368+
if: always()
369+
run: |
370+
set -euo pipefail
371+
ARTIFACT_DIR="${{ github.workspace }}/.github/scripts/mcp-smoke/artifacts"
372+
RESULTS_FILE="$ARTIFACT_DIR/claude/results.json"
373+
{
374+
echo "## Claude Smoke"
375+
echo
376+
echo "- job result: ${{ job.status }}"
377+
} >> "$GITHUB_STEP_SUMMARY"
378+
379+
if [ ! -f "$RESULTS_FILE" ]; then
380+
{
381+
echo
382+
echo "No results.json was produced."
383+
} >> "$GITHUB_STEP_SUMMARY"
384+
exit 0
385+
fi
386+
387+
total=$(jq 'length' "$RESULTS_FILE")
388+
passed=$(jq '[.[] | select(.status == "pass")] | length' "$RESULTS_FILE")
389+
skipped=$(jq '[.[] | select(.status == "skip")] | length' "$RESULTS_FILE")
390+
failed=$(jq '[.[] | select(.status == "fail")] | length' "$RESULTS_FILE")
391+
392+
{
393+
echo
394+
echo "| Case | Expected Tool | Status | Reason |"
395+
echo "|---|---|---|---|"
396+
jq -r '.[] | "| \(.case_id) | \(.expected_tool) | \(.status) | \(.reason) |"' "$RESULTS_FILE"
397+
echo
398+
echo "- Total checks: ${total}"
399+
echo "- Passed: ${passed}"
400+
echo "- Skipped: ${skipped}"
401+
echo "- Failed: ${failed}"
402+
} >> "$GITHUB_STEP_SUMMARY"
403+
404+
- name: Upload Claude artifacts
405+
if: always()
406+
uses: actions/upload-artifact@v7
407+
with:
408+
name: mcp-ai-smoke-claude
409+
path: .github/scripts/mcp-smoke/artifacts/claude
410+
if-no-files-found: warn
411+
297412
summary:
298413
name: Final Summary
299414
runs-on: ubuntu-24.04
300-
needs: [preflight, smoke_gemini, smoke_codex]
415+
needs: [preflight, smoke_gemini, smoke_codex, smoke_claude]
301416
if: always()
302417
steps:
303418
- name: Download Gemini artifacts
@@ -314,6 +429,13 @@ jobs:
314429
name: mcp-ai-smoke-codex
315430
path: .github/scripts/mcp-smoke/artifacts/codex
316431

432+
- name: Download Claude artifacts
433+
if: needs.smoke_claude.result != 'skipped'
434+
uses: actions/download-artifact@v5
435+
with:
436+
name: mcp-ai-smoke-claude
437+
path: .github/scripts/mcp-smoke/artifacts/claude
438+
317439
- name: Final status summary
318440
run: |
319441
set -euo pipefail
@@ -359,9 +481,11 @@ jobs:
359481
echo "- preflight: ${{ needs.preflight.outputs.should_run }}"
360482
echo "- Gemini available: ${{ needs.preflight.outputs.has_gemini }}"
361483
echo "- Codex available: ${{ needs.preflight.outputs.has_codex }}"
484+
echo "- Claude available: ${{ needs.preflight.outputs.has_claude }}"
362485
echo "- reason: ${{ needs.preflight.outputs.reason }}"
363486
echo "- Gemini job result: ${{ needs.smoke_gemini.result || 'skipped' }}"
364487
echo "- Codex job result: ${{ needs.smoke_codex.result || 'skipped' }}"
488+
echo "- Claude job result: ${{ needs.smoke_claude.result || 'skipped' }}"
365489
echo
366490
echo "Prototype smoke is intentionally non-blocking."
367491
} >> "$GITHUB_STEP_SUMMARY"
@@ -373,3 +497,7 @@ jobs:
373497
if [ "${{ needs.preflight.outputs.has_codex }}" = "true" ]; then
374498
summarize_provider codex
375499
fi
500+
501+
if [ "${{ needs.preflight.outputs.has_claude }}" = "true" ]; then
502+
summarize_provider claude
503+
fi

0 commit comments

Comments
 (0)