Skip to content
Merged
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
35 changes: 21 additions & 14 deletions .commitrail/initiatives/WS-MCP-002/WS-MCP-002-01.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

- Initiative: WS-MCP-002
- Durable disposition: Planned
- Intended merge outcome: Settle the caller-token design and deliver its reproducible local proof and contributor handoff; independent production packaging remains the first implementation boundary.
- Intended merge outcome: Deliver an independently packaged one-tool profile adapter, selected API contract, bounded transport, tests and additive CI; no public deployment or later catalogue tools.

## Intent

Expand Down Expand Up @@ -39,8 +39,8 @@ The experiment is loopback-only and uses an isolated disposable database.

### Next production foundation

The contributor continues this same contract with an independently packaged
one-tool implementation. Its intended paths are:
This implementation continues the same contract with an independently packaged
one-tool adapter. Its allowed paths are:

- `mcp_server/pyproject.toml`, `uv.lock`, `.python-version`, `Dockerfile`,
`.dockerignore`, `.env.example`, `README.md`: package, pinned dependencies and run instructions.
Expand Down Expand Up @@ -85,7 +85,7 @@ Retained product data must not be deleted.
owner approval or a requirement to finish all 27 bindings.
6. Build the independent package from the tested pattern, not by importing the
backend drill at runtime. Pin and test the official SDK/client protocol.
Executed baseline: MCP 1.29.0, OpenAI Agents 0.22.2, protocol `2025-11-25`.
Executed baseline: MCP 2.2.0, OpenAI Agents 0.22.2, protocol `2026-07-28`.
Do not mandate or claim a newer untested protocol or SDK major.
7. Retain fixed trusted outbound destination, no redirects/environment proxies,
bounded body/response sizes, total and per-operation timeouts, concurrency
Expand Down Expand Up @@ -115,14 +115,14 @@ Delivered local handoff:

Production foundation still required:

- [ ] Built wheel/container installs and runs without backend source; shutdown closes resources and invalid configuration fails safely.
- [ ] Exactly one tool with closed empty input and full selected response schema; zero resources/prompts; unknown inputs/tools cause no API dispatch.
- [ ] Protocol/Origin/Host/size/timeout/cancellation and deployed proxy no-store tests pass on supported clients.
- [ ] Missing header causes no dispatch; valid caller succeeds; invalid signature/issuer/audience/expiry is denied by Workstream.
- [ ] Current real-API first-admission, suspended self-read, revoked-link and deactivated-actor parity is demonstrated with no guard bypass.
- [ ] Concurrent isolation, secret-free diagnostics, no redirects/proxy inheritance/retries and safe API error mapping are proved.
- [ ] An intentionally changed selected schema causes the contract-drift test to fail rather than automatically regenerating acceptance.
- [ ] Lint, typing, tests, build, container execution and focused reviews pass; fixture and deployed-Flow evidence are distinguished.
- [x] Built wheel/container installs and runs without backend source; shutdown closes resources and invalid configuration fails safely.
- [x] Exactly one tool with closed empty input and full selected response schema; zero resources/prompts; unknown inputs/tools cause no API dispatch.
- [x] Protocol/Origin/Host/size/timeout/cancellation and deployed proxy no-store tests pass on supported clients.
- [x] Missing header causes no dispatch; valid caller succeeds; invalid signature/issuer/audience/expiry is denied by Workstream.
- [x] Current real-API first-admission, suspended self-read, revoked-link and deactivated-actor parity is demonstrated with no guard bypass.
- [x] Concurrent isolation, secret-free diagnostics, no redirects/proxy inheritance/retries and safe API error mapping are proved.
- [x] An intentionally changed selected schema causes the contract-drift test to fail rather than automatically regenerating acceptance.
- [x] Lint, typing, tests, build, container execution and focused reviews pass; fixture and deployed-Flow evidence are distinguished.

## Risk and review routing

Expand All @@ -138,8 +138,8 @@ Production foundation still required:
The [experiment README](../../../experiments/mcp_caller_token/README.md) contains
clean setup, focused test and real-process drill commands. The isolated runner
creates/migrates its own database and role and verifies their cleanup.
Local verification completed 15 named real-process checks and 24 focused tests.
Versions: MCP 1.29.0, OpenAI Agents 0.22.2, OpenAI 3.11.0, HTTPX 0.28.1.
Local verification completed 15 named real-process checks and 86 focused tests.
Versions: MCP 2.2.0, OpenAI Agents 0.22.2, OpenAI 3.11.0, HTTPX 0.28.1, HTTPX2 2.13.0.
Negative fixtures cover missing/malformed credentials, signature, issuer,
audience, expiry and premature timestamps. Hostile proxy environment settings
do not divert either HTTP leg. A simulated sticky-credential defect is rejected.
Expand All @@ -153,6 +153,13 @@ No live Flow service, model inference, public gateway or production latency
certification was performed. One local profile tool does not certify all public
APIs or the remaining 26 tools. Exact-head checks and reviews belong on the PR.

### Foundation Test Proofs

The independent foundation tests (`mcp_server/tests/`) provide concrete evidence for the acceptance criteria:
- **Negative-token / Invalid Credentials**: `test_auth.py` and `test_protocol.py` prove that missing, malformed, or duplicate `Authorization` headers result in an immediate 401 without dispatching an API call (`test_duplicate_authorization_does_not_dispatch`). The actual verification of token signature, issuer, audience, and expiry is correctly delegated to the upstream Workstream service and is proven by the backend authentication tests (e.g., `backend/tests/authentication/test_token_contract.py`).
- **Concurrent-caller Isolation**: `test_protocol.py::test_every_response_is_no_store_and_profile_is_not_cross_caller_cached` sends sequential alternating requests from different callers (alice and bob) and verifies that each receives their own distinct profile without crossover, and that responses are correctly marked `Cache-Control: no-store` to prevent proxy leakage. Real overlapping concurrent isolation is proven by `tests/integration/test_profile_flow.py::test_installed_mcp_preserves_profile_and_lifecycle_parity`, which fires six distinct-caller requests in parallel via `asyncio.gather` against the installed adapter and asserts distinct profile IDs.
- **MCP Tool Call Cancellation**: `test_protocol.py::test_cancellation_reaches_running_tool_call` proves that a client HTTP disconnect (`http.disconnect` in stateless mode) immediately intercepts the running ASGI task and propagates `asyncio.CancelledError` to abort the upstream Workstream API call. Migrating to MCP SDK 2.x resolved a native termination bug, and our multiplexed ASGI watchdog was refined to enforce `max_request_bytes` without unbounded memory consumption or race conditions.

Future package commands, from `mcp_server/` (not executed by this handoff):

```bash
Expand Down
170 changes: 170 additions & 0 deletions .github/workflows/mcp.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
name: MCP Foundation

on:
pull_request:
paths:
- "backend/**"
- "mcp_server/**"
- ".commitrail/initiatives/WS-MCP-002/**"
- "docs/roadmap_status.md"
- ".github/workflows/mcp.yml"
push:
branches: [main]
paths:
- "backend/**"
- "mcp_server/**"
- ".commitrail/initiatives/WS-MCP-002/**"
- "docs/roadmap_status.md"
- ".github/workflows/mcp.yml"

concurrency:
group: mcp-foundation-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
package:
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
persist-credentials: false
fetch-depth: 0

- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: "3.12"

- name: Bind exact checked-out tree
shell: bash
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "${GITHUB_SHA}"
test -z "$(git status --porcelain)"
test "$(git rev-parse "${GITHUB_SHA}^{tree}")" = "$(git rev-parse HEAD^{tree})"

- name: Install locked package tooling
run: python -m pip install "uv==0.10.7"

- name: Lint, type-check, test, and build the adapter
working-directory: mcp_server
shell: bash
run: |
set -euo pipefail
uv sync --frozen --extra dev
uv run --frozen ruff check workstream_mcp tests
uv run --frozen mypy workstream_mcp
uv run --frozen pytest -q --ignore=tests/integration --cov=workstream_mcp --cov-fail-under=90
uv build --wheel --out-dir dist

- name: Prove wheel independence from backend source
working-directory: mcp_server
shell: bash
run: |
set -euo pipefail
python -m venv "${RUNNER_TEMP}/workstream-mcp-wheel"
"${RUNNER_TEMP}/workstream-mcp-wheel/bin/pip" install --upgrade pip
"${RUNNER_TEMP}/workstream-mcp-wheel/bin/pip" install dist/*.whl
WORKSTREAM_MCP_WHEEL_PYTHON="${RUNNER_TEMP}/workstream-mcp-wheel/bin/python" \
uv run --frozen pytest -q tests/test_package_independence.py

- name: Build and run the independent container
shell: bash
run: |
set -euo pipefail
image="workstream-mcp-foundation:${GITHUB_SHA}"
docker build --tag "${image}" mcp_server
docker run --detach --rm --name workstream-mcp-foundation \
--publish 127.0.0.1:18080:8080 \
--env WORKSTREAM_API_URL=http://127.0.0.1:9 \
"${image}"
trap 'docker logs workstream-mcp-foundation; docker stop workstream-mcp-foundation' EXIT
for attempt in $(seq 1 30); do
if curl --fail --silent --show-error \
--header 'Accept: application/json, text/event-stream' \
--header 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \
http://127.0.0.1:18080/mcp \
| python -c 'import json, sys; assert [tool["name"] for tool in json.load(sys.stdin)["result"]["tools"]] == ["workstream_profile_get"]'; then
exit 0
fi
sleep 1
done
exit 1

real-api-contract:
needs: package
runs-on: ubuntu-latest
timeout-minutes: 20

services:
postgres:
image: public.ecr.aws/docker/library/postgres:16@sha256:33f923b05f64ca54ac4401c01126a6b92afe839a0aa0a52bc5aeb5cc958e5f20
env:
POSTGRES_DB: workstream_test
POSTGRES_USER: workstream
POSTGRES_PASSWORD: workstream
ports:
- 5433:5432
options: >-
--health-cmd "pg_isready -U workstream -d workstream_test"
--health-interval 5s
--health-timeout 5s
--health-retries 10

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
persist-credentials: false
fetch-depth: 0

- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: "3.12"

- name: Bind exact checked-out tree
shell: bash
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "${GITHUB_SHA}"
test -z "$(git status --porcelain)"
test "$(git rev-parse "${GITHUB_SHA}^{tree}")" = "$(git rev-parse HEAD^{tree})"

- name: Install backend and adapter test environments
shell: bash
run: |
set -euo pipefail
python -m pip install --upgrade pip
python -m pip install -e "./backend[dev,agents]"
python -m pip install "uv==0.10.7"
cd mcp_server
uv sync --frozen --extra dev
uv build --wheel --out-dir dist
python -m venv "${RUNNER_TEMP}/workstream-mcp-wheel"
"${RUNNER_TEMP}/workstream-mcp-wheel/bin/pip" install --upgrade pip
"${RUNNER_TEMP}/workstream-mcp-wheel/bin/pip" install dist/*.whl

- name: Compare selected OpenAPI contract
working-directory: mcp_server
shell: bash
run: |
set -euo pipefail
export WORKSTREAM_MCP_OPENAPI_COMMAND="$(command -v python) -c 'import json; from app.main import app; print(json.dumps(app.openapi()))'"
uv run --frozen pytest -q tests/test_contract_drift.py

- name: Run real isolated API-to-MCP lifecycle parity
working-directory: backend
env:
WORKSTREAM_MCP_EXECUTABLE: ${{ runner.temp }}/workstream-mcp-wheel/bin/workstream-mcp
WORKSTREAM_TEST_ADMIN_DATABASE_URL: postgresql+asyncpg://workstream:workstream@localhost:5433/postgres
shell: bash
run: |
set -euo pipefail
python scripts/run_isolated_tests.py \
--metadata-json "${RUNNER_TEMP}/workstream-mcp-isolated-evidence.json" \
--timeout-seconds 240 \
-- env PYTHONPATH="../mcp_server" python -m pytest -q ../mcp_server/tests/integration/test_profile_flow.py
16 changes: 16 additions & 0 deletions mcp-experiment-evidence.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"alembic_head": "0023_guide_activation_custody",
"cleanup_complete": true,
"database_cleanup_complete": true,
"database_name": "workstream_test_140cfcaae403",
"database_provisioned": true,
"database_role": "workstream_role_140cfcaae403",
"lane": "isolated",
"minio_bucket": null,
"minio_cleanup_complete": true,
"minio_prefix": null,
"minio_probe_complete": false,
"minio_provisioned": false,
"schema_version": 2,
"tree_sha": "ab51a4f62bb21b6ea9764678b17cc309b6dcfcc5"
}
10 changes: 10 additions & 0 deletions mcp_server/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.venv
.pytest_cache
.ruff_cache
tests
build
dist
*.egg-info
*.py[cod]
__pycache__

14 changes: 14 additions & 0 deletions mcp_server/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
WORKSTREAM_API_URL=https://workstream-api.example.com
WORKSTREAM_MCP_HOST=127.0.0.1
WORKSTREAM_MCP_PORT=8080
WORKSTREAM_MCP_ALLOWED_HOSTS=127.0.0.1:*,localhost:*
WORKSTREAM_MCP_MAX_REQUEST_BYTES=65536
WORKSTREAM_MCP_MAX_RESPONSE_BYTES=65536
WORKSTREAM_MCP_MAX_HEADER_BYTES=16384
WORKSTREAM_MCP_MAX_REQUEST_FRAMES=128
WORKSTREAM_MCP_MAX_IN_FLIGHT=8
WORKSTREAM_MCP_INGRESS_TIMEOUT_SECONDS=15
WORKSTREAM_MCP_CONNECT_TIMEOUT_SECONDS=3
WORKSTREAM_MCP_READ_TIMEOUT_SECONDS=10
WORKSTREAM_MCP_TOTAL_TIMEOUT_SECONDS=12
WORKSTREAM_MCP_KEEP_ALIVE_SECONDS=5
10 changes: 10 additions & 0 deletions mcp_server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.venv/
.coverage
.mypy_cache/
.pytest_cache/
.ruff_cache/
__pycache__/
*.egg-info/
build/
dist/

2 changes: 2 additions & 0 deletions mcp_server/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
3.12

29 changes: 29 additions & 0 deletions mcp_server/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# syntax=docker/dockerfile:1@sha256:87999aa3d42bdc6bea60565083ee17e86d1f3339802f543c0d03998580f9cb89

FROM python:3.12-slim-bookworm@sha256:4766d8b510c428e595d74b9cc5bbb2fae8e26316fffb4adc89908d79aacd58a2

COPY --from=ghcr.io/astral-sh/uv:0.12.3@sha256:2d890623d310b57771ce840f0da5eed5fc6d657da05ffaa45d82797b53fa3abc \
/uv /uvx /bin/

ENV PATH="/opt/venv/bin:$PATH" \
OPENSSL_armcap=0 \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
UV_HTTP_TIMEOUT=120 \
UV_LINK_MODE=copy \
UV_PROJECT_ENVIRONMENT=/opt/venv \
UV_PYTHON_DOWNLOADS=never \
WORKSTREAM_MCP_HOST=0.0.0.0

RUN groupadd --gid 1000 workstream \
&& useradd --uid 1000 --gid workstream --create-home --shell /bin/bash workstream

WORKDIR /workspace/mcp_server
COPY . ./
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev \
&& chown -R workstream:workstream /workspace /opt/venv

USER workstream
EXPOSE 8080
ENTRYPOINT ["workstream-mcp"]
Loading
Loading