Skip to content

mcp-test-v1.1.0

Choose a tag to compare

@github-actions github-actions released this 02 May 23:34
· 29 commits to main since this release
27e74c6

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 call

capture_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_params and response_result carry GIN jsonb_path_ops indexes (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.2

With 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

  1. CODECOV_TOKEN repo secret (Settings -> Secrets and variables -> Actions). Without it the Codecov upload step skips cleanly; with it, every PR gets a coverage diff report.
  2. Disable default-config CodeQL (Settings -> Code security -> Code scanning -> CodeQL analysis -> "None" or "Custom"). The explicit codeql.yml workflow 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_bytes or set audit.capture_payloads: false for high-throughput deployments.
  • Backwards compatibility. Public Go API is unchanged: audit.Logger, mcpmw.Audit, audit.Event all work as before. The new audit.Payload type and the mcpmw.AuditOption variadics are purely additive.
  • Existing audit_events rows. Pre-v1.1.0 rows still render correctly through the portal API; their payload field is simply omitted.
  • Database migration. Runs automatically on boot via golang-migrate. The 0002_audit_payloads.up.sql migration adds the new table and indexes; reverts cleanly via 0002_audit_payloads.down.sql if needed.

Installation

Container

docker pull ghcr.io/plexara/mcp-test:v1.1.0

The 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.0

Binary (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 --version

Documentation

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.