Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/dogfood-sse.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Optional live SSE dogfood capture (#65).
# Skips the job when TOOL_SEMANTICS_DOGFOOD_SSE_URL secret is unset.

name: Dogfood SSE

on:
workflow_dispatch:
schedule:
- cron: "0 6 * * 1"

permissions:
contents: read

jobs:
capture:
if: ${{ vars.ENABLE_DOGFOOD_SSE == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.12"
cache: pip
- run: python -m pip install --upgrade pip
- run: pip install -e ".[dev]"
- name: Live SSE capture (integration)
env:
TOOL_SEMANTICS_DOGFOOD_SSE_URL: ${{ secrets.TOOL_SEMANTICS_DOGFOOD_SSE_URL }}
TOOL_SEMANTICS_DOGFOOD_SSE_TOKEN: ${{ secrets.TOOL_SEMANTICS_DOGFOOD_SSE_TOKEN }}
run: pytest -m integration tests/test_dogfood_sse.py
51 changes: 46 additions & 5 deletions docs/downstream.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,60 @@ that is not covered by `ProbeMetrics` / `StabilityReport`.

## market-pulse-mcp — dogfood SSE capture

Optional live-capture smoke against the org MCP server:
Optional live-capture smoke against the org MCP server (or any SSE MCP endpoint).
**Capture-only** — discovered tools are never executed.

### Manual / nightly recipe

```bash
export TOOL_SEMANTICS_DOGFOOD_SSE_URL="$MARKET_PULSE_SSE_URL" # required
export TOOL_SEMANTICS_DOGFOOD_SSE_TOKEN="$TOKEN" # if auth required

pip install "tool-semantics>=0.4.0"
tool-semantics capture-mcp --sse "$MARKET_PULSE_SSE_URL" \
--header "Authorization: Bearer $TOKEN" \
# CLI path
tool-semantics capture-mcp --sse "$TOOL_SEMANTICS_DOGFOOD_SSE_URL" \
--header "Authorization: Bearer $TOOL_SEMANTICS_DOGFOOD_SSE_TOKEN" \
-o .tool-semantics/market-pulse.json

# Integration test (skipped in PR CI when env is unset)
pytest -m integration tests/test_dogfood_sse.py
```

Aliases accepted by the test: `MARKET_PULSE_SSE_URL`, `MARKET_PULSE_SSE_TOKEN` / `TOKEN`.

Optional GitHub Actions job (enable when repository secrets exist):

```yaml
# .github/workflows/dogfood-sse.yml
name: Dogfood SSE
on:
workflow_dispatch:
schedule:
- cron: "0 6 * * 1" # weekly Monday 06:00 UTC
jobs:
capture:
if: ${{ secrets.TOOL_SEMANTICS_DOGFOOD_SSE_URL != '' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.12"
- run: pip install -e ".[dev]"
- run: pytest -m integration tests/test_dogfood_sse.py
env:
TOOL_SEMANTICS_DOGFOOD_SSE_URL: ${{ secrets.TOOL_SEMANTICS_DOGFOOD_SSE_URL }}
TOOL_SEMANTICS_DOGFOOD_SSE_TOKEN: ${{ secrets.TOOL_SEMANTICS_DOGFOOD_SSE_TOKEN }}
```

Confirm:

- Snapshot protocol / metadata transport is SSE (`mcp-sse` / `sse`)
- Auth header values never appear in snapshot JSON / metadata
- Snapshot protocol is `mcp-sse` (metadata `transport: sse`)
- Auth header **values** never appear in snapshot JSON / metadata (only header names)
- Tools list is non-empty and redaction still applies

Automated assertions live in [`tests/test_dogfood_sse.py`](../tests/test_dogfood_sse.py)
(`pytest.mark.integration`). Default PR CI does not set credentials, so the test
skips and stays green.

No market-pulse-mcp code changes are required for this dogfood path.
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ packages = ["src/tool_semantics"]
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-q"
markers = [
"integration: live external services; skipped unless dogfood env credentials are set",
]

[tool.ruff]
line-length = 100
Expand Down
74 changes: 74 additions & 0 deletions tests/test_dogfood_sse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Optional live dogfood capture against a real MCP SSE server (#65).

Skipped in default PR CI when credentials are absent. Capture-only — never
executes discovered tools. Tokens come from env / CI secrets and must not appear
in snapshot JSON.
"""

from __future__ import annotations

import json
import os
import re

import pytest

from tool_semantics.mcp_capture import capture_mcp_sse

pytestmark = pytest.mark.integration

_URL_ENV = "TOOL_SEMANTICS_DOGFOOD_SSE_URL"
_TOKEN_ENV = "TOOL_SEMANTICS_DOGFOOD_SSE_TOKEN"
# Also accept the market-pulse names documented in docs/downstream.md
_URL_ENV_ALIASES = ("MARKET_PULSE_SSE_URL",)
_TOKEN_ENV_ALIASES = ("MARKET_PULSE_SSE_TOKEN", "TOKEN")


def _env_first(*names: str) -> str | None:
for name in names:
value = os.environ.get(name)
if value:
return value
return None


def _require_dogfood_creds() -> tuple[str, str | None]:
url = _env_first(_URL_ENV, *_URL_ENV_ALIASES)
token = _env_first(_TOKEN_ENV, *_TOKEN_ENV_ALIASES)
if not url:
pytest.skip(f"Set {_URL_ENV} (or MARKET_PULSE_SSE_URL) to run live SSE dogfood capture")
return url, token


def _assert_no_secrets_in_snapshot(payload: dict[str, object], token: str | None) -> None:
dumped = json.dumps(payload, default=str)
if token:
assert token not in dumped
# Common Bearer forms
assert f"Bearer {token}" not in dumped
# Auth header values must never be persisted; only header *names* are OK.
assert not re.search(r'"authorization"\s*:\s*"Bearer ', dumped, re.IGNORECASE)


def test_live_dogfood_sse_capture_only() -> None:
"""Capture tools from a live SSE MCP server; never call tools/call."""
url, token = _require_dogfood_creds()
headers: dict[str, str] = {}
if token:
headers["Authorization"] = f"Bearer {token}"

snapshot = capture_mcp_sse(url, headers=headers or None, timeout=30.0, redact=True)

assert snapshot.protocol == "mcp-sse"
assert snapshot.metadata.get("transport") == "sse"
assert len(snapshot.tools) > 0

payload = json.loads(snapshot.model_dump_json(by_alias=True))
_assert_no_secrets_in_snapshot(payload, token)

# Capture path only lists interface — ensure we did not invent tool results.
assert "tool_results" not in payload
assert snapshot.metadata.get("request_header_names") is not None
header_names = snapshot.metadata.get("request_header_names")
if token and isinstance(header_names, list):
assert any(str(name).lower() == "authorization" for name in header_names)