From fbdc85cd6309fbb216277654d5142f4f1163d624 Mon Sep 17 00:00:00 2001 From: Laurel Orr Date: Wed, 22 Apr 2026 15:21:33 -0700 Subject: [PATCH 1/5] ci: remove Claude workflows and fix Renovate dhi.io lookups The Claude review workflows (claude.yml, claude-code-review.yml) triggered on every PR via ANTHROPIC_API_KEY. On a public repo, drive-by PRs can burn that budget; the template should not ship with opinionated review tooling bound to one provider. renovate.json adds a packageRule disabling dhi.io/** Docker lookups. Renovate cannot introspect dhi.io without credentials, which was causing the "Action Required: Fix Renovate Configuration" failure that blocked all dependency PRs. DHI image pins are now maintained manually after docker-pull verification. --- .github/workflows/claude-code-review.yml | 68 ------------------------ .github/workflows/claude.yml | 63 ---------------------- renovate.json | 10 +++- 3 files changed, 9 insertions(+), 132 deletions(-) delete mode 100644 .github/workflows/claude-code-review.yml delete mode 100644 .github/workflows/claude.yml diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml deleted file mode 100644 index d999f70..0000000 --- a/.github/workflows/claude-code-review.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: Claude Code Review - -on: - workflow_call: - # Uncomment the following to trigger on PR creation or updates. - # pull_request: - # Only make CC review on PR creation. Later, if we want further checks we can mention it. - # e.g. @claude please re-review - # types: [opened] - # types: [opened, synchronize] - # Optional: Only run on specific file changes - # paths: - # - "src/**/*.ts" - # - "src/**/*.tsx" - # - "src/**/*.js" - # - "src/**/*.jsx" - -jobs: - claude-review: - # Optional: Filter by PR author - # if: | - # github.event.pull_request.user.login == 'external-contributor' || - # github.event.pull_request.user.login == 'new-developer' || - # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' - - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - issues: read - id-token: write - - steps: - - name: Checkout repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - fetch-depth: 1 - - - name: Run Claude Code Review - id: claude-review - uses: anthropics/claude-code-action@ac1a3207f3f00b4a37e2f3a6f0935733c7c64651 # v1.0 - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - prompt: | - REPO: ${{ github.repository }} - PR NUMBER: ${{ github.event.pull_request.number }} - - Please review this pull request and provide feedback on: - - Code quality and best practices - - Potential bugs or issues - - Performance considerations - - Security concerns - - Regressions or breaking changes - - Verbosity and clarity of code. The code should be as short as possible while still being clear. - - Use the repository's CLAUDE.md for guidance on style and conventions. - - Have the following practices in mind while reviewing: - - Be constructive and helpful in your feedback. - - Be specific and precise in the feedback. Reading time for the review should be less than 2 minutes. - - Format your review as a structured comment. The action will post it automatically. - - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://docs.claude.com/en/docs/claude-code/cli-reference for available options - # Read-only tools only — no Bash write operations to prevent prompt injection via PR content - claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"' - diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml deleted file mode 100644 index aa67714..0000000 --- a/.github/workflows/claude.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Claude Code - -on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - issues: - types: [opened, assigned] - pull_request_review: - types: [submitted] - -jobs: - claude: - # Restrict to repository members/owners to prevent prompt injection from external users - if: |- - (github.event_name == 'issue_comment' && - contains(github.event.comment.body, '@claude') && - contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), - github.event.comment.author_association)) || - (github.event_name == 'pull_request_review_comment' && - contains(github.event.comment.body, '@claude') && - contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), - github.event.comment.author_association)) || - (github.event_name == 'pull_request_review' && - contains(github.event.review.body, '@claude') && - contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), - github.event.review.author_association)) || - (github.event_name == 'issues' && - (contains(github.event.issue.body, '@claude') || - contains(github.event.issue.title, '@claude')) && - contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), - github.event.issue.author_association)) - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - issues: write - id-token: write - actions: read # Required for Claude to read CI results on PRs - steps: - - name: Checkout repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - fetch-depth: 1 - - - name: Run Claude Code - id: claude - uses: anthropics/claude-code-action@ac1a3207f3f00b4a37e2f3a6f0935733c7c64651 # v1.0 - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - - # This is an optional setting that allows Claude to read CI results on PRs - additional_permissions: | - actions: read - - # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. - # prompt: 'Update the pull request description to include a summary of changes.' - - # Restrict to read-only tools to limit blast radius of prompt injection - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*),Bash(gh run view:*),Bash(gh run list:*)"' - diff --git a/renovate.json b/renovate.json index db2b58c..08e40ee 100644 --- a/renovate.json +++ b/renovate.json @@ -8,5 +8,13 @@ "prConcurrentLimit": 3, "schedule": ["after 1am and before 7am every weekday"], "timezone": "America/Los_Angeles", - "minimumReleaseAge": "3 days" + "minimumReleaseAge": "3 days", + "packageRules": [ + { + "description": "DHI images live on dhi.io and require authentication; Renovate cannot introspect the registry without credentials. Pin Dockerfile bumps manually after verifying with docker pull.", + "matchDatasources": ["docker"], + "matchPackageNames": ["dhi.io/**"], + "enabled": false + } + ] } From f5f90b1f913b8f8231d804b1c3f8d832a6e87e15 Mon Sep 17 00:00:00 2001 From: Laurel Orr Date: Wed, 22 Apr 2026 15:21:41 -0700 Subject: [PATCH 2/5] ci: polish release workflow stubs for template consumers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three release workflows (release.yml, create-release.yml, patch-release.yml) ship intentionally stubbed so the template repo itself does not publish artifacts. The previous TODO comments were ambiguous ("Replace PLACEHOLDER" without saying where, mixing the stub state with real config). Rewrite the top-of-file block in each workflow to spell out the exact unstub steps (swap the trigger, delete the stub guard, follow docs/release-playbook.md for required secrets). No behavior change — the files remain inert until a fork owner unstubs them. --- .github/workflows/create-release.yml | 22 ++++++++++++-------- .github/workflows/patch-release.yml | 30 ++++++++++++++++++---------- .github/workflows/release.yml | 14 +++++++++++-- 3 files changed, 45 insertions(+), 21 deletions(-) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index f559d8c..e47c406 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -1,15 +1,21 @@ # Manually triggered workflow that creates a release branch, bumps the version -# in VERSION and pyproject.toml, and pushes a tag — which fires the main -# release.yml pipeline. +# in VERSION and pyproject.toml, and pushes a tag — which fires release.yml. # -# Uses a GitHub App token (MCP_RELEASE_WORKFLOW_APP_ID + MCP_RELEASE_WORKFLOW_APP_KEY secrets) so that the tag -# push triggers downstream workflows. GITHUB_TOKEN cannot do this due to a -# GitHub Actions security restriction. +# Requires a GitHub App token (secrets MCP_RELEASE_WORKFLOW_APP_ID and +# MCP_RELEASE_WORKFLOW_APP_KEY). The default GITHUB_TOKEN cannot fire +# downstream workflows on tag push — GitHub explicitly blocks that to prevent +# infinite workflow recursion. # -# TODO: This is a template repo — uncomment the workflow_dispatch trigger and -# remove the stub trigger below when using this template in a real project. +# This workflow ships STUBBED so the template repo itself does not cut +# releases. To enable in your fork: +# 1. Replace the bare `on: workflow_dispatch` trigger below with the richer +# one (with inputs) that is commented out. +# 2. Remove the `if: false` guard on the `create-release` job. +# 3. See `docs/release-playbook.md` for the full list of required secrets. + name: Create Release +# Real trigger — uncomment when enabling releases in your fork: # on: # workflow_dispatch: # inputs: @@ -29,7 +35,7 @@ permissions: {} jobs: create-release: - # TODO: Remove this condition when enabling the real trigger above. + # Stub guard — delete this line when enabling the real trigger above. if: false runs-on: ubuntu-latest permissions: diff --git a/.github/workflows/patch-release.yml b/.github/workflows/patch-release.yml index a15573f..2b66d21 100644 --- a/.github/workflows/patch-release.yml +++ b/.github/workflows/patch-release.yml @@ -1,15 +1,23 @@ -# Automatically creates a patch release when a PR is merged to a release branch. -# Bumps VERSION and pyproject.toml, commits, tags, and pushes — which triggers -# release.yml. +# Automatically creates a patch release when a PR is merged to a release/X.Y +# branch. Bumps VERSION and pyproject.toml, commits, tags, and pushes — which +# triggers release.yml. # -# Uses a GitHub App token (MCP_RELEASE_WORKFLOW_APP_ID + MCP_RELEASE_WORKFLOW_APP_KEY secrets) so that the tag -# push triggers downstream workflows. GITHUB_TOKEN cannot do this due to a -# GitHub Actions security restriction. +# Requires a GitHub App token (secrets MCP_RELEASE_WORKFLOW_APP_ID and +# MCP_RELEASE_WORKFLOW_APP_KEY). The default GITHUB_TOKEN cannot fire +# downstream workflows on tag push — GitHub explicitly blocks that to prevent +# infinite workflow recursion. # -# TODO: This is a template repo — uncomment the pull_request trigger and -# remove the stub trigger below when using this template in a real project. +# This workflow ships STUBBED so the template repo itself does not cut +# releases. To enable in your fork: +# 1. Replace the `on: workflow_dispatch` trigger below with the +# `on: pull_request` block that is commented out. +# 2. Uncomment the `concurrency` block. +# 3. Replace `if: false` with `if: github.event.pull_request.merged == true`. +# 4. See `docs/release-playbook.md` for the full list of required secrets. + name: Patch Release +# Real trigger — uncomment when enabling releases in your fork: # on: # pull_request: # types: [closed] @@ -20,15 +28,15 @@ on: permissions: {} -# TODO: Uncomment concurrency when enabling the real trigger above. +# Uncomment when enabling the real trigger above: # concurrency: # group: patch-release-${{ github.event.pull_request.base.ref }} # cancel-in-progress: false jobs: patch-release: - # TODO: Remove this condition when enabling the real trigger above, - # and restore: if: github.event.pull_request.merged == true + # Stub guard — replace with `if: github.event.pull_request.merged == true` + # when enabling the real trigger above. if: false runs-on: ubuntu-latest permissions: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e076131..d363c12 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,17 @@ +# Release pipeline: build multi-arch image, sign with Cosign, attest SLSA +# provenance, and create a GitHub Release with auto-generated notes. +# +# This workflow ships STUBBED so the template repo itself does not publish +# releases. To enable in your fork: +# 1. Replace the `on: workflow_dispatch` trigger with the `on: push: tags` +# block that is commented out below. +# 2. Replace every `PLACEHOLDER` in this file (image name, OCI title, OCI +# description) with your values. +# 3. See `docs/release-playbook.md` for the full list of required secrets. + name: Release -# TODO: Replace the placeholder trigger below with the real one when using this template. -# Replace all occurrences of 'PLACEHOLDER' with the appropriate values. +# Real trigger — uncomment when enabling releases in your fork: # on: # push: # tags: From f463bae4557aa99d644d509fc3446a2b8cc8eefe Mon Sep 17 00:00:00 2001 From: Laurel Orr Date: Wed, 22 Apr 2026 15:21:56 -0700 Subject: [PATCH 3/5] chore(deps): bump dependencies and pin CVE fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fresh pip-audit flagged two new transitive CVEs: - lxml 6.0.2 (via cyclonedx-bom) — CVE-2026-41066, fixed in 6.1.0 - python-dotenv 1.2.1 (via pydantic-settings) — CVE-2026-28684, fixed in 1.2.2 Both pinned at the project level following the existing pattern for cryptography and python-multipart. Direct deps (fastapi, mcp, pydantic, pydantic-settings) and dev/security tooling (ruff, ty, pytest-asyncio, pytest-cov, bandit, pip-audit, cyclonedx-bom) all bumped to current fix-floors to close out the open Renovate backlog in a single pass. The ty bump (0.0.1a18 floor -> 0.0.32 actual) surfaced two pre-existing type issues in the test suite: - test_mcp.py used hasattr(item, "text") which ty cannot narrow; switch to isinstance(item, TextContent) from mcp.types - test_settings.py used a # type: ignore comment ty did not recognize; switch to Settings.model_validate({...}) which is typed end-to-end After these fixes pip-audit reports 0 vulnerabilities and task check passes cleanly. The stale CVE-2025-71176 comment on the pytest pin was also removed since the pin has long since outrun that advisory. --- pyproject.toml | 24 +-- .../auth/mcp_auth_middleware.py | 4 +- tests/integration/test_mcp.py | 5 +- tests/unit/test_settings.py | 3 +- tests/unit/test_token_passthrough.py | 8 +- uv.lock | 143 +++++++++--------- 6 files changed, 94 insertions(+), 93 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 124c4ff..39b16fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,10 +6,10 @@ readme = "README.md" requires-python = ">=3.13" dependencies = [ "cryptography>=46.0.7", # CVE-2026-39892 - transitive dep via mcp->pyjwt[crypto] - "fastapi>=0.127.0", - "mcp>=1.22.0", - "pydantic>=2.12.4", - "pydantic-settings>=2.12.0", + "fastapi>=0.135.3", + "mcp>=1.27.0", + "pydantic>=2.12.5", + "pydantic-settings>=2.13.1", "python-dotenv>=1.2.2", # CVE-2026-28684 - transitive dep via pydantic-settings "python-multipart>=0.0.26", # CVE-2026-40347 - transitive dep via fastapi "structlog>=25.5.0", @@ -17,16 +17,16 @@ dependencies = [ [dependency-groups] dev = [ - "pytest>=9.0.3", # CVE-2025-71176 - "pytest-asyncio>=1.1.0", - "ruff>=0.12.9", - "ty>=0.0.1a18", - "pytest-cov>=7.0.0", + "pytest>=9.0.3", + "pytest-asyncio>=1.3.0", + "ruff>=0.15.9", + "ty>=0.0.28", + "pytest-cov>=7.1.0", ] security = [ - "bandit[toml]>=1.8.0", - "pip-audit>=2.7.3", - "cyclonedx-bom>=6.0.0", + "bandit[toml]>=1.9.4", + "pip-audit>=2.10.0", + "cyclonedx-bom>=7.3.0", "lxml>=6.1.0", # CVE-2026-41066 - transitive dep via cyclonedx-bom ] diff --git a/src/mcp_template_py/auth/mcp_auth_middleware.py b/src/mcp_template_py/auth/mcp_auth_middleware.py index b6c3149..31f6737 100644 --- a/src/mcp_template_py/auth/mcp_auth_middleware.py +++ b/src/mcp_template_py/auth/mcp_auth_middleware.py @@ -32,9 +32,7 @@ async def dispatch(self, request: Request, call_next): token = auth_header[7:] if self.require_bearer_token and token is None: - return JSONResponse( - {"detail": "Bearer token required"}, status_code=401 - ) + return JSONResponse({"detail": "Bearer token required"}, status_code=401) ctx_token = _current_bearer_token.set(token) try: diff --git a/tests/integration/test_mcp.py b/tests/integration/test_mcp.py index c4d0001..4b6290b 100644 --- a/tests/integration/test_mcp.py +++ b/tests/integration/test_mcp.py @@ -7,6 +7,7 @@ import os import pytest +from mcp.types import TextContent from tests.integration.conftest import get_mcp_client_session @@ -58,7 +59,9 @@ async def test_mcp_client_call_hello_tool(self): assert result is not None assert len(result.content) > 0 - text_contents = [item for item in result.content if hasattr(item, "text")] + text_contents = [ + item for item in result.content if isinstance(item, TextContent) + ] assert any("Hello, Alice!" in item.text for item in text_contents), ( f"Expected greeting not found in response: {[item.text for item in text_contents]}" ) diff --git a/tests/unit/test_settings.py b/tests/unit/test_settings.py index f264419..ffb08b9 100644 --- a/tests/unit/test_settings.py +++ b/tests/unit/test_settings.py @@ -10,7 +10,8 @@ def test_default_settings(self): assert settings.server_url == "http://localhost:8100" def test_debug_parsed_from_string(self): - settings = Settings(debug="true") # type: ignore[arg-type] # pydantic coerces via validator + # pydantic-settings coerces string env values via validator + settings = Settings.model_validate({"debug": "true"}) assert settings.debug is True def test_debug_parsed_from_bool(self): diff --git a/tests/unit/test_token_passthrough.py b/tests/unit/test_token_passthrough.py index afbccb0..37b94b8 100644 --- a/tests/unit/test_token_passthrough.py +++ b/tests/unit/test_token_passthrough.py @@ -30,7 +30,9 @@ def client() -> TestClient: Route("/error", raise_error), ], middleware=[ - Middleware(cast(Any, TokenPassthroughMiddleware), require_bearer_token=False) + Middleware( + cast(Any, TokenPassthroughMiddleware), require_bearer_token=False + ) ], ) return TestClient(app, raise_server_exceptions=False) @@ -92,7 +94,5 @@ def test_valid_token_passes_through(self, strict_client: TestClient): assert response.json()["token"] == "my-token" def test_non_bearer_auth_returns_401(self, strict_client: TestClient): - response = strict_client.get( - "/test", headers={"Authorization": "Basic abc123"} - ) + response = strict_client.get("/test", headers={"Authorization": "Basic abc123"}) assert response.status_code == 401 diff --git a/uv.lock b/uv.lock index 252c526..e7d8aca 100644 --- a/uv.lock +++ b/uv.lock @@ -56,7 +56,7 @@ wheels = [ [[package]] name = "bandit" -version = "1.9.2" +version = "1.9.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -64,9 +64,9 @@ dependencies = [ { name = "rich" }, { name = "stevedore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cf/72/f704a97aac430aeb704fa16435dfa24fbeaf087d46724d0965eb1f756a2c/bandit-1.9.2.tar.gz", hash = "sha256:32410415cd93bf9c8b91972159d5cf1e7f063a9146d70345641cd3877de348ce", size = 4241659, upload-time = "2025-11-23T21:36:18.722Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c3/0cb80dfe0f3076e5da7e4c5ad8e57bac6ac357ff4a6406205501cade4965/bandit-1.9.4.tar.gz", hash = "sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628", size = 4242677, upload-time = "2026-02-25T06:44:15.503Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/1a/5b0320642cca53a473e79c7d273071b5a9a8578f9e370b74da5daa2768d7/bandit-1.9.2-py3-none-any.whl", hash = "sha256:bda8d68610fc33a6e10b7a8f1d61d92c8f6c004051d5e946406be1fb1b16a868", size = 134377, upload-time = "2025-11-23T21:36:17.39Z" }, + { url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" }, ] [[package]] @@ -337,7 +337,7 @@ wheels = [ [[package]] name = "cyclonedx-bom" -version = "7.2.1" +version = "7.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "chardet" }, @@ -346,9 +346,9 @@ dependencies = [ { name = "packaging" }, { name = "pip-requirements-parser" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/b4/d6a3eee8622389893480758ada629842b8667e326ec8da311dbc7f5087f4/cyclonedx_bom-7.2.1.tar.gz", hash = "sha256:ead9923a23c71426bcc83ea371c87945b85f76c31728625dde35ecfe0fa2e712", size = 4416994, upload-time = "2025-10-29T15:31:47.238Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/e1/700aea05811f8988a60636e045914ffb155ce5318879f14b5c9016a12da5/cyclonedx_bom-7.3.0.tar.gz", hash = "sha256:d54080d65731980945de38e52009a5e5711f4a3c31377a3d13af96eaca5fcf3d", size = 4420319, upload-time = "2026-03-30T12:30:08.438Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/3a/c30b624eb2b5f33d9f5a55f23a65f529c875897639961cf51d2af8a5e527/cyclonedx_bom-7.2.1-py3-none-any.whl", hash = "sha256:fdeabfec4f3274085320a40d916fc4dc2850abef7da5953d544eb5c98aa4afdd", size = 60696, upload-time = "2025-10-29T15:31:45.594Z" }, + { url = "https://files.pythonhosted.org/packages/c6/f8/285a990b17b44dac4899b14fbeab81a28f3bb828621d4a6c449631249136/cyclonedx_bom-7.3.0-py3-none-any.whl", hash = "sha256:73d7d76b3a28ebadc50eabf424cbcf128785a74b8a59ce7893da2e29cfaab585", size = 60924, upload-time = "2026-03-30T12:30:06.943Z" }, ] [[package]] @@ -384,17 +384,18 @@ wheels = [ [[package]] name = "fastapi" -version = "0.127.0" +version = "0.136.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0c/02/2cbbecf6551e0c1a06f9b9765eb8f7ae126362fbba43babbb11b0e3b7db3/fastapi-0.127.0.tar.gz", hash = "sha256:5a9246e03dcd1fdb19f1396db30894867c1d630f5107dc167dcbc5ed1ea7d259", size = 369269, upload-time = "2025-12-21T16:47:16.393Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/d9/e66315807e41e69e7f6a1b42a162dada2f249c5f06ad3f1a95f84ab336ef/fastapi-0.136.0.tar.gz", hash = "sha256:cf08e067cc66e106e102d9ba659463abfac245200752f8a5b7b1e813de4ff73e", size = 396607, upload-time = "2026-04-16T11:47:13.623Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/fa/6a27e2ef789eb03060abb43b952a7f0bd39e6feaa3805362b48785bcedc5/fastapi-0.127.0-py3-none-any.whl", hash = "sha256:725aa2bb904e2eff8031557cf4b9b77459bfedd63cae8427634744fd199f6a49", size = 112055, upload-time = "2025-12-21T16:47:14.757Z" }, + { url = "https://files.pythonhosted.org/packages/26/a3/0bd5f0cdb0bbc92650e8dc457e9250358411ee5d1b65e42b6632387daf81/fastapi-0.136.0-py3-none-any.whl", hash = "sha256:8793d44ec7378e2be07f8a013cf7f7aa47d6327d0dfe9804862688ec4541a6b4", size = 117556, upload-time = "2026-04-16T11:47:11.922Z" }, ] [[package]] @@ -637,7 +638,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.23.3" +version = "1.27.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -655,9 +656,9 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/a4/d06a303f45997e266f2c228081abe299bbcba216cb806128e2e49095d25f/mcp-1.23.3.tar.gz", hash = "sha256:b3b0da2cc949950ce1259c7bfc1b081905a51916fcd7c8182125b85e70825201", size = 600697, upload-time = "2025-12-09T16:04:37.351Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/eb/c0cfc62075dc6e1ec1c64d352ae09ac051d9334311ed226f1f425312848a/mcp-1.27.0.tar.gz", hash = "sha256:d3dc35a7eec0d458c1da4976a48f982097ddaab87e278c5511d5a4a56e852b83", size = 607509, upload-time = "2026-04-02T14:48:08.88Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/c6/13c1a26b47b3f3a3b480783001ada4268917c9f42d78a079c336da2e75e5/mcp-1.23.3-py3-none-any.whl", hash = "sha256:32768af4b46a1b4f7df34e2bfdf5c6011e7b63d7f1b0e321d0fdef4cd6082031", size = 231570, upload-time = "2025-12-09T16:04:35.56Z" }, + { url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" }, ] [[package]] @@ -693,10 +694,10 @@ security = [ [package.metadata] requires-dist = [ { name = "cryptography", specifier = ">=46.0.7" }, - { name = "fastapi", specifier = ">=0.127.0" }, - { name = "mcp", specifier = ">=1.22.0" }, - { name = "pydantic", specifier = ">=2.12.4" }, - { name = "pydantic-settings", specifier = ">=2.12.0" }, + { name = "fastapi", specifier = ">=0.135.3" }, + { name = "mcp", specifier = ">=1.27.0" }, + { name = "pydantic", specifier = ">=2.12.5" }, + { name = "pydantic-settings", specifier = ">=2.13.1" }, { name = "python-dotenv", specifier = ">=1.2.2" }, { name = "python-multipart", specifier = ">=0.0.26" }, { name = "structlog", specifier = ">=25.5.0" }, @@ -705,16 +706,16 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "pytest", specifier = ">=9.0.3" }, - { name = "pytest-asyncio", specifier = ">=1.1.0" }, - { name = "pytest-cov", specifier = ">=7.0.0" }, - { name = "ruff", specifier = ">=0.12.9" }, - { name = "ty", specifier = ">=0.0.1a18" }, + { name = "pytest-asyncio", specifier = ">=1.3.0" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, + { name = "ruff", specifier = ">=0.15.9" }, + { name = "ty", specifier = ">=0.0.28" }, ] security = [ - { name = "bandit", extras = ["toml"], specifier = ">=1.8.0" }, - { name = "cyclonedx-bom", specifier = ">=6.0.0" }, + { name = "bandit", extras = ["toml"], specifier = ">=1.9.4" }, + { name = "cyclonedx-bom", specifier = ">=7.3.0" }, { name = "lxml", specifier = ">=6.1.0" }, - { name = "pip-audit", specifier = ">=2.7.3" }, + { name = "pip-audit", specifier = ">=2.10.0" }, ] [[package]] @@ -943,16 +944,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.12.0" +version = "2.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/98/c8345dccdc31de4228c039a98f6467a941e39558da41c1744fbe29fa5666/pydantic_settings-2.14.0.tar.gz", hash = "sha256:24285fd4b0e0c06507dd9fdfd331ee23794305352aaec8fc4eb92d4047aeb67d", size = 235709, upload-time = "2026-04-20T13:37:40.293Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, + { url = "https://files.pythonhosted.org/packages/01/dd/bebff3040138f00ae8a102d426b27349b9a49acc310fcae7f92112d867e3/pydantic_settings-2.14.0-py3-none-any.whl", hash = "sha256:fc8d5d692eb7092e43c8647c1c35a3ecd00e040fcf02ed86f4cb5458ca62182e", size = 60940, upload-time = "2026-04-20T13:37:38.586Z" }, ] [[package]] @@ -1017,16 +1018,16 @@ wheels = [ [[package]] name = "pytest-cov" -version = "7.0.0" +version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage" }, { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] [[package]] @@ -1250,28 +1251,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ed/d9/f7a0c4b3a2bf2556cd5d99b05372c29980249ef71e8e32669ba77428c82c/ruff-0.14.8.tar.gz", hash = "sha256:774ed0dd87d6ce925e3b8496feb3a00ac564bea52b9feb551ecd17e0a23d1eed", size = 5765385, upload-time = "2025-12-04T15:06:17.669Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/b8/9537b52010134b1d2b72870cc3f92d5fb759394094741b09ceccae183fbe/ruff-0.14.8-py3-none-linux_armv6l.whl", hash = "sha256:ec071e9c82eca417f6111fd39f7043acb53cd3fde9b1f95bbed745962e345afb", size = 13441540, upload-time = "2025-12-04T15:06:14.896Z" }, - { url = "https://files.pythonhosted.org/packages/24/00/99031684efb025829713682012b6dd37279b1f695ed1b01725f85fd94b38/ruff-0.14.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8cdb162a7159f4ca36ce980a18c43d8f036966e7f73f866ac8f493b75e0c27e9", size = 13669384, upload-time = "2025-12-04T15:06:51.809Z" }, - { url = "https://files.pythonhosted.org/packages/72/64/3eb5949169fc19c50c04f28ece2c189d3b6edd57e5b533649dae6ca484fe/ruff-0.14.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e2fcbefe91f9fad0916850edf0854530c15bd1926b6b779de47e9ab619ea38f", size = 12806917, upload-time = "2025-12-04T15:06:08.925Z" }, - { url = "https://files.pythonhosted.org/packages/c4/08/5250babb0b1b11910f470370ec0cbc67470231f7cdc033cee57d4976f941/ruff-0.14.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9d70721066a296f45786ec31916dc287b44040f553da21564de0ab4d45a869b", size = 13256112, upload-time = "2025-12-04T15:06:23.498Z" }, - { url = "https://files.pythonhosted.org/packages/78/4c/6c588e97a8e8c2d4b522c31a579e1df2b4d003eddfbe23d1f262b1a431ff/ruff-0.14.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c87e09b3cd9d126fc67a9ecd3b5b1d3ded2b9c7fce3f16e315346b9d05cfb52", size = 13227559, upload-time = "2025-12-04T15:06:33.432Z" }, - { url = "https://files.pythonhosted.org/packages/23/ce/5f78cea13eda8eceac71b5f6fa6e9223df9b87bb2c1891c166d1f0dce9f1/ruff-0.14.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d62cb310c4fbcb9ee4ac023fe17f984ae1e12b8a4a02e3d21489f9a2a5f730c", size = 13896379, upload-time = "2025-12-04T15:06:02.687Z" }, - { url = "https://files.pythonhosted.org/packages/cf/79/13de4517c4dadce9218a20035b21212a4c180e009507731f0d3b3f5df85a/ruff-0.14.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1af35c2d62633d4da0521178e8a2641c636d2a7153da0bac1b30cfd4ccd91344", size = 15372786, upload-time = "2025-12-04T15:06:29.828Z" }, - { url = "https://files.pythonhosted.org/packages/00/06/33df72b3bb42be8a1c3815fd4fae83fa2945fc725a25d87ba3e42d1cc108/ruff-0.14.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25add4575ffecc53d60eed3f24b1e934493631b48ebbc6ebaf9d8517924aca4b", size = 14990029, upload-time = "2025-12-04T15:06:36.812Z" }, - { url = "https://files.pythonhosted.org/packages/64/61/0f34927bd90925880394de0e081ce1afab66d7b3525336f5771dcf0cb46c/ruff-0.14.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c943d847b7f02f7db4201a0600ea7d244d8a404fbb639b439e987edcf2baf9a", size = 14407037, upload-time = "2025-12-04T15:06:39.979Z" }, - { url = "https://files.pythonhosted.org/packages/96/bc/058fe0aefc0fbf0d19614cb6d1a3e2c048f7dc77ca64957f33b12cfdc5ef/ruff-0.14.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb6e8bf7b4f627548daa1b69283dac5a296bfe9ce856703b03130732e20ddfe2", size = 14102390, upload-time = "2025-12-04T15:06:46.372Z" }, - { url = "https://files.pythonhosted.org/packages/af/a4/e4f77b02b804546f4c17e8b37a524c27012dd6ff05855d2243b49a7d3cb9/ruff-0.14.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7aaf2974f378e6b01d1e257c6948207aec6a9b5ba53fab23d0182efb887a0e4a", size = 14230793, upload-time = "2025-12-04T15:06:20.497Z" }, - { url = "https://files.pythonhosted.org/packages/3f/52/bb8c02373f79552e8d087cedaffad76b8892033d2876c2498a2582f09dcf/ruff-0.14.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e5758ca513c43ad8a4ef13f0f081f80f08008f410790f3611a21a92421ab045b", size = 13160039, upload-time = "2025-12-04T15:06:49.06Z" }, - { url = "https://files.pythonhosted.org/packages/1f/ad/b69d6962e477842e25c0b11622548df746290cc6d76f9e0f4ed7456c2c31/ruff-0.14.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f74f7ba163b6e85a8d81a590363bf71618847e5078d90827749bfda1d88c9cdf", size = 13205158, upload-time = "2025-12-04T15:06:54.574Z" }, - { url = "https://files.pythonhosted.org/packages/06/63/54f23da1315c0b3dfc1bc03fbc34e10378918a20c0b0f086418734e57e74/ruff-0.14.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eed28f6fafcc9591994c42254f5a5c5ca40e69a30721d2ab18bb0bb3baac3ab6", size = 13469550, upload-time = "2025-12-04T15:05:59.209Z" }, - { url = "https://files.pythonhosted.org/packages/70/7d/a4d7b1961e4903bc37fffb7ddcfaa7beb250f67d97cfd1ee1d5cddb1ec90/ruff-0.14.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:21d48fa744c9d1cb8d71eb0a740c4dd02751a5de9db9a730a8ef75ca34cf138e", size = 14211332, upload-time = "2025-12-04T15:06:06.027Z" }, - { url = "https://files.pythonhosted.org/packages/5d/93/2a5063341fa17054e5c86582136e9895db773e3c2ffb770dde50a09f35f0/ruff-0.14.8-py3-none-win32.whl", hash = "sha256:15f04cb45c051159baebb0f0037f404f1dc2f15a927418f29730f411a79bc4e7", size = 13151890, upload-time = "2025-12-04T15:06:11.668Z" }, - { url = "https://files.pythonhosted.org/packages/02/1c/65c61a0859c0add13a3e1cbb6024b42de587456a43006ca2d4fd3d1618fe/ruff-0.14.8-py3-none-win_amd64.whl", hash = "sha256:9eeb0b24242b5bbff3011409a739929f497f3fb5fe3b5698aba5e77e8c833097", size = 14537826, upload-time = "2025-12-04T15:06:26.409Z" }, - { url = "https://files.pythonhosted.org/packages/6d/63/8b41cea3afd7f58eb64ac9251668ee0073789a3bc9ac6f816c8c6fef986d/ruff-0.14.8-py3-none-win_arm64.whl", hash = "sha256:965a582c93c63fe715fd3e3f8aa37c4b776777203d8e1d8aa3cc0c14424a4b99", size = 13634522, upload-time = "2025-12-04T15:06:43.212Z" }, +version = "0.15.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, + { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, ] [[package]] @@ -1378,27 +1378,26 @@ wheels = [ [[package]] name = "ty" -version = "0.0.1a32" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/92/8da015685fb83734a2a83de02080e64d182509de77fa9bcf3eed12eeab4b/ty-0.0.1a32.tar.gz", hash = "sha256:12f62e8a3dd0eaeb9557d74b1c32f0616ae40eae10a4f411e1e2a73225f67ff2", size = 4689151, upload-time = "2025-12-05T21:04:26.885Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/e6/fdc35c9ba047f16afdfedf36fb51c221e0190ccde9f70ee28e77084d6612/ty-0.0.1a32-py3-none-linux_armv6l.whl", hash = "sha256:ffe595eaf616f06f58f951766477830a55c2502d2c9f77dde8f60d9a836e0645", size = 9673128, upload-time = "2025-12-05T21:04:17.702Z" }, - { url = "https://files.pythonhosted.org/packages/19/20/eaff31048e2f309f37478f7d715c8de9f9bab03cba4758da27b9311147af/ty-0.0.1a32-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:07f1dce88ad6028fb14665aefe4e6697012c34bd48edd37d02b7eb6a833dbf62", size = 9434094, upload-time = "2025-12-05T21:04:03.383Z" }, - { url = "https://files.pythonhosted.org/packages/67/d4/ea8ed57d11b81c459f23561fd6bfb0f54a8d4120cf72541e3bdf71d46202/ty-0.0.1a32-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8fab7ed12528c77ddd600a9638ca859156a53c20f1e381353fa87a255bd397eb", size = 8980296, upload-time = "2025-12-05T21:04:28.912Z" }, - { url = "https://files.pythonhosted.org/packages/49/02/3ce98bbfbb3916678d717ee69358d38a404ca9a39391dda8874b66dd5ee7/ty-0.0.1a32-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ace395280fc21e25eff0a53cfbd68170f90a4b8ef2f85dfabe1ecbca2ced456b", size = 9263054, upload-time = "2025-12-05T21:04:05.619Z" }, - { url = "https://files.pythonhosted.org/packages/b7/be/a639638bcd1664de2d70a87da6c4fe0e3272a60b7fa3f0c108a956a456bd/ty-0.0.1a32-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2bcbeed7f5ed8e3c1c7e525fce541e7b943ac04ee7fe369a926551b5e50ea4a8", size = 9451396, upload-time = "2025-12-05T21:04:01.265Z" }, - { url = "https://files.pythonhosted.org/packages/1f/a4/2bcf54e842a3d10dc14b369f28a3bab530c5d7ddba624e910b212bda93ee/ty-0.0.1a32-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60ff2e4493f90f81a260205d87719bb1d3420928a1e4a2a7454af7cbdfed2047", size = 9862726, upload-time = "2025-12-05T21:04:08.806Z" }, - { url = "https://files.pythonhosted.org/packages/5f/c7/19e6719496e59f2f082f34bcac312698366cf50879fdcc3ef76298bfe6a0/ty-0.0.1a32-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:53cad50a59a0d943b06872e0b10f9f2b564805c2ea93f64c7798852bc1901954", size = 10475051, upload-time = "2025-12-05T21:04:31.059Z" }, - { url = "https://files.pythonhosted.org/packages/88/77/bdf0ddb066d2b62f141d058f8a33bb7c8628cdbb8bfa75b20e296b79fb4e/ty-0.0.1a32-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:343d43cdc1d7f649ea2baa64ac2b479da3d679239b94509f1df12f7211561ea9", size = 10232712, upload-time = "2025-12-05T21:04:19.849Z" }, - { url = "https://files.pythonhosted.org/packages/ed/07/f73260a461762a581a007015c1019d40658828ce41576f8c1db88dee574d/ty-0.0.1a32-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f45483e4a84bcf622413712164ea687ce323a9f7013b9e7977c5d623ed937ca9", size = 10237705, upload-time = "2025-12-05T21:04:35.366Z" }, - { url = "https://files.pythonhosted.org/packages/2c/57/dbb92206cf2f798d8c51ea16504e8afb90a139d0ff105c31cec9a1db29f9/ty-0.0.1a32-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d452f30d47002a6bafc36d1b6aee42c321e9ec9f7f43a04a2ee7d48c208b86c", size = 9766469, upload-time = "2025-12-05T21:04:22.236Z" }, - { url = "https://files.pythonhosted.org/packages/c3/5e/143d93bd143abcebcbaa98c8aeec78898553d62d0a5a432cd79e0cf5bd6d/ty-0.0.1a32-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:86c4e31737fe954637890cef1f3e1b479ffb20e836cac3b76050bdbe80005010", size = 9238592, upload-time = "2025-12-05T21:04:11.33Z" }, - { url = "https://files.pythonhosted.org/packages/21/b8/225230ae097ed88f3c92ad974dd77f8e4f86f2594d9cd0c729da39769878/ty-0.0.1a32-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:daf15fa03bc39a76a0fbc9c2d81d79d528f584e3fbe08d71981e3f7912db91d6", size = 9502161, upload-time = "2025-12-05T21:04:37.642Z" }, - { url = "https://files.pythonhosted.org/packages/85/13/cc89955c9637f25f3aca2dd7749c6008639ef036f0b9bea3e9d89e892ff9/ty-0.0.1a32-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6128f6bab5c6dab3d08689fed1d529dc34f50f221f89c8e16064ed0c549dad7a", size = 9603058, upload-time = "2025-12-05T21:04:39.532Z" }, - { url = "https://files.pythonhosted.org/packages/46/77/1fe2793c8065a02d1f70ca7da1b87db49ca621bcbbdb79a18ad79d5d0ab2/ty-0.0.1a32-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:55aab688be1b46776a5a458a1993cae0da7725932c45393399c479c2fa979337", size = 9879903, upload-time = "2025-12-05T21:04:13.567Z" }, - { url = "https://files.pythonhosted.org/packages/fc/47/fd58e80a3e42310b4b649340d5d97403fe796146cae8678b3a031a414b8e/ty-0.0.1a32-py3-none-win32.whl", hash = "sha256:f55ec25088a09236ad1578b656a07fa009c3a353f5923486905ba48175d142a6", size = 9077703, upload-time = "2025-12-05T21:04:15.849Z" }, - { url = "https://files.pythonhosted.org/packages/8d/96/209c417c69317339ea8e9b3277fd98364a0e97dd1ffd3585e143ec7b4e57/ty-0.0.1a32-py3-none-win_amd64.whl", hash = "sha256:ed8d5cbd4e47dfed86aaa27e243008aa4e82b6a5434f3ab95c26d3ee5874d9d7", size = 9922426, upload-time = "2025-12-05T21:04:33.289Z" }, - { url = "https://files.pythonhosted.org/packages/e0/1c/350fd851fb91244f8c80cec218009cbee7564d76c14e2f423b47e69a5cbc/ty-0.0.1a32-py3-none-win_arm64.whl", hash = "sha256:dbb25f9b513d34cee8ce419514eaef03313f45c3f7ab4eb6e6d427ea1f6854af", size = 9453761, upload-time = "2025-12-05T21:04:24.502Z" }, +version = "0.0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/7e/2aa791c9ae7b8cd5024cd4122e92267f664ca954cea3def3211919fa3c1f/ty-0.0.32.tar.gz", hash = "sha256:8743174c5f920f6700a4a0c9de140109189192ba16226884cd50095b43b8a45c", size = 5522294, upload-time = "2026-04-20T19:29:01.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/eb/1075dc6a49d7acbe2584ae4d5b410c41b1f177a5adcc567e09eca4c69000/ty-0.0.32-py3-none-linux_armv6l.whl", hash = "sha256:dacbc2f6cd698d488ae7436838ff929570455bf94bfa4d9fe57a630c552aff83", size = 10902959, upload-time = "2026-04-20T19:28:31.907Z" }, + { url = "https://files.pythonhosted.org/packages/33/d2/c35fc8bc66e98d1ee9b0f8ed319bf743e450e1f1e997574b178fab75670f/ty-0.0.32-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914bbc4f605ce2a9e2a78982e28fae1d3359a169d141f9dc3b4c7749cd5eca81", size = 10726172, upload-time = "2026-04-20T19:28:44.765Z" }, + { url = "https://files.pythonhosted.org/packages/96/32/c827da3ca480456fb02d8cea68a2609273b6c220fea0be9a4c8d8470b86e/ty-0.0.32-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4787ac9fe1f86b1f3133f5c6732adbe2df5668b50c679ac6e2d98cd284da812f", size = 10163701, upload-time = "2026-04-20T19:28:27.005Z" }, + { url = "https://files.pythonhosted.org/packages/ba/9e/2734478fbdb90c160cb2813a3916a16a2af5c1e231f87d635f6131d781fb/ty-0.0.32-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ea0a728af99fe40dd744cba6441a2404f80b7f4bde17aa6da393810af5ea57", size = 10656220, upload-time = "2026-04-20T19:29:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/44/9f/0007da2d35e424debe7e9f86ffbc1ab7f60983cfbc5f0411324ab2de5292/ty-0.0.32-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2850561f9b018ae33d7e5bbfa0ac414d3c518513edcffe43877dc9801446b9c5", size = 10696086, upload-time = "2026-04-20T19:28:46.829Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5e/ce5fd4ec803222ae3e69a76d2a2db2eed55e19f5b131702b9789ef45f93d/ty-0.0.32-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b5fa2fb3c614349ee211d36476b49d88c5ef79a687cdb91b2872ad023b94d2f8", size = 11184800, upload-time = "2026-04-20T19:28:42.57Z" }, + { url = "https://files.pythonhosted.org/packages/6c/46/ebcf67a5999421331214aac51a7464db42de2be15bbe929c612a3ed0b039/ty-0.0.32-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b89969307ab2417d41c9be8059dd79feea577234e1e10d35132f5495e0d42c6", size = 11718718, upload-time = "2026-04-20T19:28:36.433Z" }, + { url = "https://files.pythonhosted.org/packages/18/2c/2141c86ed0ce0962b45cefb658a95e734f59759d47f20afdcd9c732910a1/ty-0.0.32-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b59868ede9b1d69a088f0d695df52a0061f95fa7baa1d5e0dc6fc9cf06e1334", size = 11346369, upload-time = "2026-04-20T19:28:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/7a/da/ed6f772339cf29bd9a46def9d6db5084689eb574ee4d150ff704224c1ed8/ty-0.0.32-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8300caf35345498e9b9b03e550bba03cee8f5f5f8ab4c83c3b1ff1b7403b7d3a", size = 11280714, upload-time = "2026-04-20T19:28:51.516Z" }, + { url = "https://files.pythonhosted.org/packages/da/9b/c6813987edf4816a40e0c8e408b555f97d3f267c7b3a1688c8bbdf65609c/ty-0.0.32-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:583c7094f4574b02f724db924f98b804d1387a0bd9405ecb5e078cc0f47fbcfb", size = 10638806, upload-time = "2026-04-20T19:28:29.651Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d4/0cefcbd2ad0f3d51762ccf58e652ec7da146eb6ae34f87228f6254bbb8be/ty-0.0.32-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e44ebe1bb4143a5628bc4db67ac0dfebe14594af671e4ee66f6f2e983da56501", size = 10726106, upload-time = "2026-04-20T19:29:06.3Z" }, + { url = "https://files.pythonhosted.org/packages/32/ad/2c8a97f91f06311f4367400f7d13534bbda2522c73c99a3e4c0757dff9b8/ty-0.0.32-py3-none-musllinux_1_2_i686.whl", hash = "sha256:06f17ada3e069cba6148342ef88e9929156beca8473e8d4f101b68f66c75643e", size = 10872951, upload-time = "2026-04-20T19:28:34.077Z" }, + { url = "https://files.pythonhosted.org/packages/ba/68/42293f9248106dd51875120971a5cc6ea315c2c4dcfb8e59aa063aa0af26/ty-0.0.32-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e96e60fa556cec04f15d7ea62d2ceee5982bd389233e961ab9fd42304e278175", size = 11363334, upload-time = "2026-04-20T19:28:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/df/92/be9abf4d3e589ad5023e2ea965b93e204ec856420d46adf73c5c36c04678/ty-0.0.32-py3-none-win32.whl", hash = "sha256:2ff2ebb4986b24aebcf1444db7db5ca41b36086040e95eea9f8fb851c11e805c", size = 10260689, upload-time = "2026-04-20T19:28:56.541Z" }, + { url = "https://files.pythonhosted.org/packages/14/61/dc86acea899349d2579cb8419aecedd83dc504d7d6a10df65eef546c8300/ty-0.0.32-py3-none-win_amd64.whl", hash = "sha256:ba7284a4a954b598c1b31500352b3ec1f89bff533825592b5958848226fdc7ee", size = 11255371, upload-time = "2026-04-20T19:28:39.917Z" }, + { url = "https://files.pythonhosted.org/packages/43/01/beffec56d71ca25b343ede63adb076456b5b3e211f1c066452a44cd120b3/ty-0.0.32-py3-none-win_arm64.whl", hash = "sha256:7e10aadbdbda989a7d567ee6a37f8b98d4d542e31e3b190a2879fd581f75d658", size = 10658087, upload-time = "2026-04-20T19:28:59.286Z" }, ] [[package]] From b5f3123ce0b3f92dd154929496ef8c421d4bf1c5 Mon Sep 17 00:00:00 2001 From: Laurel Orr Date: Wed, 22 Apr 2026 15:22:13 -0700 Subject: [PATCH 4/5] docs: public-release polish Getting the template ready to ship as a public repo: - README rewritten for a public audience: reframe DHI as free-with-Docker-Hub-account (it went GA-free in late 2025, not a paid SKU), add a "For fork maintainers" section spelling out the DOCKERHUB_USERNAME/TOKEN and GitHub App release secrets, link the existing release playbook and contribution guide, fix a stale "safety" tool mention that was never actually wired up. - SECURITY.md casing: stackloklabs -> StacklokLabs (the lowercase URL 404s). - CONTRIBUTING.md added: setup steps, task check, Conventional Commits + DCO sign-off, pointer to the org-level Code of Conduct in StacklokLabs/.github. - Taskfile.yml: task run was invoking "python -m src.mcp_template_py" while the Dockerfile and README use "python -m mcp_template_py". Align the Taskfile so "task run" actually works without the src. prefix. --- CONTRIBUTING.md | 51 ++++++++++++++++++++++++++++++++++++++++++++++ README.md | 54 +++++++++++++++++++++++++++++++++++++++---------- SECURITY.md | 4 ++-- Taskfile.yml | 4 +++- 4 files changed, 99 insertions(+), 14 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..77011d0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,51 @@ +# Contributing to mcp-template-py + +Thanks for your interest! This repo is a template — most contributions will be improvements to the template itself (new MCP tooling patterns, better docs, CI/CD hardening). + +## Development setup + +```bash +# Prerequisites: Python 3.13+, uv, Task (see README) +task install +cp .env.example .env +task run +``` + +## Before opening a PR + +Run the full check suite locally. It mirrors CI: + +```bash +task check # lint + format + typecheck + test + security +``` + +Individual tasks are listed in `README.md`. All of them must pass before CI will. + +## PR conventions + +- **Title format:** [Conventional Commits](https://www.conventionalcommits.org/) — enforced by `lint-pr-title.yml`. Examples: + - `feat(tools): add a rate-limited search tool` + - `fix(auth): handle missing Bearer token header` + - `docs: clarify DHI setup` + - `chore: bump dependencies` +- **Scope:** keep PRs focused. Smaller PRs review faster. +- **Tests:** unit tests in `tests/unit/` (alongside the module), integration tests in `tests/integration/`. + +## Developer Certificate of Origin (DCO) + +All commits must be signed off, certifying that you have the right to submit the contribution: + +```bash +git commit -s -m "your message" +``` + +The `-s` flag appends a `Signed-off-by` trailer. See the [DCO](https://developercertificate.org/) for the full text. + +## Reporting issues + +- **Bugs / feature requests:** open an issue. +- **Security vulnerabilities:** see [SECURITY.md](SECURITY.md) — do not file a public issue. + +## Code of Conduct + +By participating in this project, you agree to abide by the [StacklokLabs Code of Conduct](https://github.com/StacklokLabs/.github/blob/main/CODE_OF_CONDUCT.md). diff --git a/README.md b/README.md index e7596b2..f7f34bd 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,16 @@ A production-ready template for building Python MCP (Model Context Protocol) ser ## What's Included -- **FastMCP server** with example tool implementation +- **FastMCP server** with an example tool implementation - **Token passthrough** — Bearer tokens from MCP clients are available to tools via context. Requests without a Bearer token are rejected with 401 by default (`REQUIRE_BEARER_TOKEN=true`); set to `false` for local development - **Pydantic** for data validation and type safety - **Task automation** via [Taskfile](https://taskfile.dev/) for common operations - **Testing infrastructure** with pytest and pytest-asyncio - **Code quality tools**: ruff (linting/formatting), ty (type checking) -- **Security scanning**: safety, bandit, pip-audit, cyclonedx-bom -- **Docker support** with multi-platform builds (amd64/arm64) using [Docker Hardened Images (DHI)](https://docs.docker.com/dhi/how-to/use/) -- **GitHub Actions** for CI/CD, code quality, and automated builds (release not included) +- **Security scanning**: bandit, pip-audit, cyclonedx-bom (SBOM), Grype (container + filesystem) +- **Hardened containers** built on [Docker Hardened Images (DHI)](https://docs.docker.com/dhi/) — multi-arch (amd64/arm64) +- **Release pipeline** (shipped as stubs): Cosign signing, SLSA provenance attestation, GitHub Releases with auto-generated notes +- **GitHub Actions** for CI/CD, code quality, and automated builds ## Quick Start @@ -45,13 +46,19 @@ task run task compose ``` -> **Note:** Building from source uses [Docker Hardened Images (DHI)](https://docs.docker.com/dhi/how-to/use/) -> which require authentication to `dhi.io`: -> 1. Create a Docker Hub account (or use your existing one) -> 2. Run `docker login dhi.io` (use your Docker Hub credentials) - The server runs on `http://0.0.0.0:8100` by default. +### DHI authentication + +The Dockerfile and CI image builds use [Docker Hardened Images (DHI)](https://docs.docker.com/dhi/). DHI is **free** (Community tier, Apache 2.0) but pulls from `dhi.io` require authentication with a Docker Hub account: + +```bash +# A free Docker Hub account works — no paid subscription required +docker login dhi.io +``` + +If you fork this template, also see [For fork maintainers](#for-fork-maintainers) below for the GitHub secrets CI needs. + ## Implementing New Tools Tools are implemented in [src/mcp_template_py/api/tools.py](src/mcp_template_py/api/tools.py) as methods on the `Tools` class, and registered in [src/mcp_template_py/api/mcp_builder.py](src/mcp_template_py/api/mcp_builder.py): @@ -102,6 +109,8 @@ mcp.add_tool(tools.hello) | `task format` | Format code and fix lint issues | | `task typecheck` | Run ty type checker | | `task test` | Run pytest tests | +| `task security` | Run bandit + pip-audit | +| `task sbom` | Generate a CycloneDX SBOM | | `task check` | Run all checks (lint, typecheck, test, security) | ## Project Structure @@ -137,10 +146,11 @@ Configuration is managed through environment variables. Copy `.env.example` to ` | `MCP_HOST` | `0.0.0.0` | Host for the MCP server to listen on | | `MCP_PORT` | `8100` | Port for the MCP server to listen on | | `SERVER_URL` | `http://localhost:8100` | Base URL of the server | +| `REQUIRE_BEARER_TOKEN` | `true` | Reject requests without a Bearer token | ## Testing -**Note**: Integration tests (`tests/integration/`) require the MCP server to be running (`task run` or `task compose`) and will be skipped if unreachable. +Integration tests (`tests/integration/`) require the MCP server to be running (`task run` or `task compose`) and will be skipped if unreachable. ```bash # Run all tests @@ -150,6 +160,28 @@ task test uv run pytest --cov=src/mcp_template_py ``` +## For Fork Maintainers + +When you fork this template for a real project, CI needs a few GitHub Actions secrets to work: + +**Required for image builds and security scans** (`image-build.yml`, `security.yml`): +- `DOCKERHUB_USERNAME` — your Docker Hub username +- `DOCKERHUB_TOKEN` — a [Docker Hub access token](https://docs.docker.com/security/for-developers/access-tokens/) with public-read scope + +**Required when you enable the release pipeline** (see [docs/release-playbook.md](docs/release-playbook.md)): +- `MCP_RELEASE_WORKFLOW_APP_ID` — numeric App ID of a GitHub App installed on your fork with Contents: Read/Write +- `MCP_RELEASE_WORKFLOW_APP_KEY` — the App's private key (full `.pem` contents) + +The release workflows (`release.yml`, `create-release.yml`, `patch-release.yml`) ship **stubbed** so the template itself does not publish artifacts. To enable them in your fork, follow the unstub steps in [docs/release-playbook.md](docs/release-playbook.md). + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, PR conventions, and the DCO sign-off requirement. + +## Security + +To report a vulnerability, please use the GitHub Security Advisory flow described in [SECURITY.md](SECURITY.md) — do not file a public issue. + ## License -See [LICENSE](LICENSE) for details. +[Apache 2.0](LICENSE). diff --git a/SECURITY.md b/SECURITY.md index f598a5c..2f06066 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -7,7 +7,7 @@ acknowledge your contributions. ## Reporting a vulnerability To report a security issue, please use the GitHub Security Advisory -["Report a Vulnerability"](https://github.com/stackloklabs/mcp-template-py/security/advisories/new) +["Report a Vulnerability"](https://github.com/StacklokLabs/mcp-template-py/security/advisories/new) tab. If you are unable to access GitHub you can also email us at @@ -78,7 +78,7 @@ These steps should be completed within the 1-7 days of Disclosure. - Create a new [security advisory](https://docs.github.com/en/code-security/security-advisories/) in the affected repository by visiting - `https://github.com/stackloklabs/mcp-template-py/security/advisories/new` + `https://github.com/StacklokLabs/mcp-template-py/security/advisories/new` - As many details as possible should be entered such as versions affected, CVE (if available yet). As more information is discovered, edit and update the advisory accordingly. diff --git a/Taskfile.yml b/Taskfile.yml index 9953cae..698367c 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -66,7 +66,9 @@ tasks: run: desc: Run the MCP server locally cmds: - - uv run python -m src.mcp_template_py + - uv run python -m mcp_template_py + deps: + - install compose: desc: Docker compose up From 14d28a69a1d6d31c82aca2dadbb38f781babe843 Mon Sep 17 00:00:00 2001 From: Laurel Orr Date: Wed, 22 Apr 2026 17:42:47 -0700 Subject: [PATCH 5/5] docs: avoid calling template consumers "forks" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review feedback from @glageju on #45: creating a new repo from a GitHub template is not a fork — template copies have no upstream link and start from a clean history. Rename the README section to "When Using This Template" and use "your own repo" / "your copy" in the release workflow comments so the language covers both template consumers (the common case) and actual forks (the contribute-back case). --- .github/workflows/create-release.yml | 4 ++-- .github/workflows/patch-release.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- README.md | 10 +++++----- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index e47c406..b9cefec 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -7,7 +7,7 @@ # infinite workflow recursion. # # This workflow ships STUBBED so the template repo itself does not cut -# releases. To enable in your fork: +# releases. To enable in your own repo: # 1. Replace the bare `on: workflow_dispatch` trigger below with the richer # one (with inputs) that is commented out. # 2. Remove the `if: false` guard on the `create-release` job. @@ -15,7 +15,7 @@ name: Create Release -# Real trigger — uncomment when enabling releases in your fork: +# Real trigger — uncomment when enabling releases in your own repo: # on: # workflow_dispatch: # inputs: diff --git a/.github/workflows/patch-release.yml b/.github/workflows/patch-release.yml index 2b66d21..c6f69a3 100644 --- a/.github/workflows/patch-release.yml +++ b/.github/workflows/patch-release.yml @@ -8,7 +8,7 @@ # infinite workflow recursion. # # This workflow ships STUBBED so the template repo itself does not cut -# releases. To enable in your fork: +# releases. To enable in your own repo: # 1. Replace the `on: workflow_dispatch` trigger below with the # `on: pull_request` block that is commented out. # 2. Uncomment the `concurrency` block. @@ -17,7 +17,7 @@ name: Patch Release -# Real trigger — uncomment when enabling releases in your fork: +# Real trigger — uncomment when enabling releases in your own repo: # on: # pull_request: # types: [closed] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d363c12..758db1b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,7 +2,7 @@ # provenance, and create a GitHub Release with auto-generated notes. # # This workflow ships STUBBED so the template repo itself does not publish -# releases. To enable in your fork: +# releases. To enable in your own repo: # 1. Replace the `on: workflow_dispatch` trigger with the `on: push: tags` # block that is commented out below. # 2. Replace every `PLACEHOLDER` in this file (image name, OCI title, OCI @@ -11,7 +11,7 @@ name: Release -# Real trigger — uncomment when enabling releases in your fork: +# Real trigger — uncomment when enabling releases in your own repo: # on: # push: # tags: diff --git a/README.md b/README.md index f7f34bd..c06798a 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ The Dockerfile and CI image builds use [Docker Hardened Images (DHI)](https://do docker login dhi.io ``` -If you fork this template, also see [For fork maintainers](#for-fork-maintainers) below for the GitHub secrets CI needs. +If you use this template for your own repo, also see [When using this template](#when-using-this-template) below for the GitHub secrets CI needs. ## Implementing New Tools @@ -160,19 +160,19 @@ task test uv run pytest --cov=src/mcp_template_py ``` -## For Fork Maintainers +## When Using This Template -When you fork this template for a real project, CI needs a few GitHub Actions secrets to work: +Whether you created a new repo via "Use this template" or actually forked this one, CI needs a few GitHub Actions secrets to work in your copy: **Required for image builds and security scans** (`image-build.yml`, `security.yml`): - `DOCKERHUB_USERNAME` — your Docker Hub username - `DOCKERHUB_TOKEN` — a [Docker Hub access token](https://docs.docker.com/security/for-developers/access-tokens/) with public-read scope **Required when you enable the release pipeline** (see [docs/release-playbook.md](docs/release-playbook.md)): -- `MCP_RELEASE_WORKFLOW_APP_ID` — numeric App ID of a GitHub App installed on your fork with Contents: Read/Write +- `MCP_RELEASE_WORKFLOW_APP_ID` — numeric App ID of a GitHub App installed on your repo with Contents: Read/Write - `MCP_RELEASE_WORKFLOW_APP_KEY` — the App's private key (full `.pem` contents) -The release workflows (`release.yml`, `create-release.yml`, `patch-release.yml`) ship **stubbed** so the template itself does not publish artifacts. To enable them in your fork, follow the unstub steps in [docs/release-playbook.md](docs/release-playbook.md). +The release workflows (`release.yml`, `create-release.yml`, `patch-release.yml`) ship **stubbed** so the template itself does not publish artifacts. To enable them in your copy, follow the unstub steps in [docs/release-playbook.md](docs/release-playbook.md). ## Contributing