Repository navigation
mcp-test-v1.1.0
Minor release. Two themes: full request/response payload capture for the audit pipeline, and a CI / project-tooling overhaul.
No breaking changes. Drop-in upgrade from v1.0.1.
Audit payload capture
The audit log now records the full request and response envelope for every tool call, not just metadata. For a server whose entire purpose is to be a fixture for testing MCP gateways, this is the diff between "I can see calls happened" and "I can see exactly what flowed through": the inputs the gateway forwarded, the outputs the server returned, the headers, the timing, the redaction state.
Capture is on by default and configurable. Operators can turn it off in privacy-sensitive deployments, cap per-call size to bound storage, or scope what's recorded.
Schema
A new audit_payloads sibling table joined 1:1 with audit_events by event_id carries the full envelope. Two-table layout keeps the indexable summary fast for time-range queries while letting operators drill into the full envelope on demand.
| Column | Contents |
|---|---|
jsonrpc_method |
The receiving-middleware-dispatched method (typically tools/call) |
request_params JSONB |
Full sanitized arguments object, GIN-indexed via jsonb_path_ops for fast containment queries |
request_size_bytes, request_truncated |
Size + flag; oversize requests are dropped wholesale and the flag is set |
request_headers JSONB |
Redacted HTTP headers, only when audit.capture_headers: true |
request_remote_addr |
Caller's network address |
response_result JSONB |
Full CallToolResult (content blocks + isError + structuredContent), GIN-indexed |
response_error JSONB |
{message, category} for failed calls |
response_size_bytes, response_truncated |
Same shape as request |
notifications JSONB |
Array of server-initiated notifications |
replayed_from |
FK to the original event when this row is a replay |
captured_at |
Forensic timestamp; supports queries when the async drain is delayed |
ON DELETE CASCADE from audit_events.id keeps retention atomic: deleting a summary row drops its payload row in the same statement. No second policy.
Configuration
Four new keys under audit::
audit:
enabled: true
retention_days: 30
redact_keys: [...]
capture_payloads: true # default
capture_headers: true # default; flip false in privacy-sensitive deployments
max_payload_bytes: 65536 # per side (request, response); larger is dropped + flagged
max_notifications: 100 # cap on recorded notifications per callcapture_payloads and capture_headers are tri-state: an omitted YAML key defaults to true, an explicit false opts out. mcp-test.example.yaml, mcp-test.dev.yaml, and mcp-test.live.yaml all surface the new keys with explanatory comments.
API
GET /api/v1/portal/audit/events/{id} returns the full event with payload joined from audit_payloads:
curl -H "X-API-Key: $MCPTEST_DEV_KEY" \
http://localhost:8080/api/v1/portal/audit/events/<event-id>Returns 404 on unknown id. Stores that don't persist payloads (in-memory, noop) return the summary alone with the payload key omitted.
audit.QueryFilter gained EventID for primary-key lookups. Both the Postgres store (PK index) and the in-memory logger honor it.
Capture middleware
mcpmw.Audit accepts variadic options:
mcpmw.Audit(chain, logger, redactKeys, toolGroups,
mcpmw.WithPayloadCapture(maxBytes),
mcpmw.WithHeaderCapture(),
mcpmw.WithMaxNotifications(n),
)The server boot wires options from cfg.Audit automatically.
Auth-failure inspection
Auth-failure rows now carry a payload row too, with a structured category in response_error.category ("auth") so the portal can filter without string-matching. Useful for triaging gateway misconfiguration: a 401 from the upstream now leaves a row that says exactly which validation step rejected the bearer.
Performance and storage
- Defaults: 64 KiB cap per side, 30-day retention. Worst case 1000 calls/day ~= 128 MB/day ~= 3.8 GB/30 days.
- Payload writes go through the same async buffered drain as summary writes; a stalled DB never inflates request latency.
request_paramsandresponse_resultcarry GINjsonb_path_opsindexes (smaller and faster than default GIN for@>containment).
CI / project tooling overhaul
mcp-test's CI surface is now at parity with the Plexara reference standard.
ci.yml split into 6 parallel jobs
| Job | What it does |
|---|---|
lint |
golangci-lint v2.11.4 |
test |
go test -race -count=1 -covermode=atomic; coverage gate at >= 80%; Codecov upload guarded by CODECOV_TOKEN presence |
build |
go build -v ./... + go mod verify |
frontend |
pnpm install + tsc --noEmit + pnpm build against ui/ so TypeScript regressions and broken Vite builds fail CI before they ship to the embedded SPA |
security |
gosec is gating (was advisory), govulncheck via the dedicated action, Semgrep with p/golang plus a project-local .semgrep/ ruleset |
integration |
go test -tags=integration ./tests/... runs the testcontainers-backed end-to-end smoke on every PR |
Per-job permissions: { contents: read } on top of workflow-level permissions: read-all. Per-job timeout-minutes. Concurrency cancellation correctly groups push and pull_request events on the same head SHA. Pure docs/markdown changes skip CI via paths-ignore.
Two new workflows
codeql.yml: CodeQL Go analysis on push/PR plus a weekly Monday cron using the security-and-quality query bundle. Findings post to the repo's Security tab.
scorecard.yml: OpenSSF Scorecard on branch_protection_rule, push to main, and a weekly Saturday cron. SARIF uploaded to code-scanning and as a 5-day artifact.
Supply-chain hardening: every action SHA-pinned
Every uses: in every workflow (ci.yml, codeql.yml, scorecard.yml, docs.yml, release.yml) is pinned to a commit SHA with a trailing version comment:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2With write + id-token permissions on the release path, floating tags would have meant a compromised action repo could silently sign and push binaries under the project's name. Pinning to bytes removes that vector.
Project-local Semgrep rules
.semgrep/go-security.yml adds two rules covering the OOM-via-user-input pattern: unbounded-make-slice-capacity and unbounded-make-map-size.
Manual repo settings recommended after upgrade
CODECOV_TOKENrepo secret (Settings -> Secrets and variables -> Actions). Without it the Codecov upload step skips cleanly; with it, every PR gets a coverage diff report.- Disable default-config CodeQL (Settings -> Code security -> Code scanning -> CodeQL analysis -> "None" or "Custom"). The explicit
codeql.ymlworkflow is the source of truth; leaving the default config on means every PR runs two identical scans.
Operational notes
- Audit storage growth. With defaults, expect roughly 3.8 GB / 30 days at 1000 calls/day worst case. Tune
audit.max_payload_bytesor setaudit.capture_payloads: falsefor high-throughput deployments. - Backwards compatibility. Public Go API is unchanged:
audit.Logger,mcpmw.Audit,audit.Eventall work as before. The newaudit.Payloadtype and themcpmw.AuditOptionvariadics are purely additive. - Existing audit_events rows. Pre-v1.1.0 rows still render correctly through the portal API; their
payloadfield is simply omitted. - Database migration. Runs automatically on boot via golang-migrate. The
0002_audit_payloads.up.sqlmigration adds the new table and indexes; reverts cleanly via0002_audit_payloads.down.sqlif needed.
Installation
Container
docker pull ghcr.io/plexara/mcp-test:v1.1.0The image ships only the binary and LICENSE; mount your own config:
docker run --rm \
-v $(pwd)/mcp-test.yaml:/app/configs/mcp-test.yaml:ro \
-p 8080:8080 \
ghcr.io/plexara/mcp-test:v1.1.0Binary (macOS / Linux)
curl -L -o mcp-test.tar.gz \
https://github.com/plexara/mcp-test/releases/download/v1.1.0/mcp-test_1.1.0_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz
tar -xzf mcp-test.tar.gz
./mcp-test --versionDocumentation
Full docs at https://mcp-test.plexara.io.
Verification
After upgrading, inspect the new capture surface:
# Make a tool call
curl -s -X POST http://localhost:8080/api/v1/admin/tryit/echo \
-H "X-API-Key: $MCPTEST_DEV_KEY" \
-H "X-Requested-With: XMLHttpRequest" \
-H "Content-Type: application/json" \
-d '{"arguments":{"message":"inspect-me"}}'
# Find the resulting event id
EVENT_ID=$(curl -s -H "X-API-Key: $MCPTEST_DEV_KEY" \
http://localhost:8080/api/v1/portal/audit/events?limit=1 \
| jq -r '.events[0].id')
# Fetch the full event, including payload
curl -s -H "X-API-Key: $MCPTEST_DEV_KEY" \
http://localhost:8080/api/v1/portal/audit/events/$EVENT_ID | jq '.payload'The response includes request_params, response_result.content, sizes, and any captured headers.
Open source by Plexara, the commercial MCP server with configurable enrichment built in.