fastmcp-creds 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ # Store and check out text files as LF on every platform, overriding a local
2
+ # core.autocrlf=true that would otherwise produce phantom EOL-only diffs.
3
+ # text=auto leaves detected binaries untouched; eol=lf pins the *working tree*.
4
+ * text=auto eol=lf
5
+
6
+ # cmd.exe reads batch files by byte offset, so LF-only endings break labels
7
+ # and goto. Do NOT add *.ps1 here: PowerShell is cross-platform, and CRLF
8
+ # breaks the shebang of a .ps1 meant to run on Linux/macOS.
9
+ *.bat text eol=crlf
10
+ *.cmd text eol=crlf
@@ -0,0 +1,40 @@
1
+ name: Publish to TestPyPI
2
+ on:
3
+ workflow_dispatch:
4
+
5
+ jobs:
6
+ test-pypi-publish:
7
+ name: Upload to TestPyPI
8
+ runs-on: ubuntu-latest
9
+ permissions:
10
+ id-token: write # For PyPI's trusted publishing
11
+ steps:
12
+ - name: Checkout
13
+ uses: actions/checkout@v5
14
+ with:
15
+ fetch-depth: 0
16
+ fetch-tags: true
17
+
18
+ - name: Install uv
19
+ uses: astral-sh/setup-uv@v6
20
+
21
+ - name: Build
22
+ run: uv build
23
+
24
+ - name: Check version format
25
+ run: |
26
+ # Extract version from the built package
27
+ VERSION=$(ls dist/*.whl | head -n1 | sed -E 's/.*-([0-9].*)-.*/\1/')
28
+ echo "Package version: $VERSION"
29
+
30
+ # Check if version contains dev/post markers (development version)
31
+ if [[ "$VERSION" =~ (dev|post.*dev) ]]; then
32
+ echo "❌ ERROR: Cannot publish development version: $VERSION"
33
+ echo "Please create a proper Git tag first (e.g., 'git tag v0.1.0')"
34
+ exit 1
35
+ fi
36
+
37
+ echo "✅ Version $VERSION is valid for publishing"
38
+
39
+ - name: Publish to TestPyPI
40
+ run: uv publish --publish-url https://test.pypi.org/legacy/ -v dist/*
@@ -0,0 +1,42 @@
1
+ name: Publish to PyPI
2
+ on:
3
+ release:
4
+ types: [published]
5
+ workflow_dispatch:
6
+
7
+ jobs:
8
+ pypi-publish:
9
+ name: Upload to PyPI
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ id-token: write # For PyPI's trusted publishing
13
+ steps:
14
+ - name: Checkout
15
+ uses: actions/checkout@v5
16
+ with:
17
+ fetch-depth: 0
18
+ fetch-tags: true
19
+
20
+ - name: Install uv
21
+ uses: astral-sh/setup-uv@v6
22
+
23
+ - name: Build
24
+ run: uv build
25
+
26
+ - name: Check version format
27
+ run: |
28
+ # Extract version from the built package
29
+ VERSION=$(ls dist/*.whl | head -n1 | sed -E 's/.*-([0-9].*)-.*/\1/')
30
+ echo "Package version: $VERSION"
31
+
32
+ # Check if version contains dev/post markers (development version)
33
+ if [[ "$VERSION" =~ (dev|post.*dev) ]]; then
34
+ echo "❌ ERROR: Cannot publish development version: $VERSION"
35
+ echo "Please create a proper Git tag first (e.g., 'git tag v0.1.0')"
36
+ exit 1
37
+ fi
38
+
39
+ echo "✅ Version $VERSION is valid for publishing"
40
+
41
+ - name: Publish to PyPI
42
+ run: uv publish -v dist/*
@@ -0,0 +1,58 @@
1
+ name: Run static analysis
2
+
3
+ env:
4
+ # Enable colored output
5
+ PY_COLORS: 1
6
+
7
+ on:
8
+ push:
9
+ branches: ["main"]
10
+ paths:
11
+ - "src/**"
12
+ - "tests/**"
13
+ - "uv.lock"
14
+ - "pyproject.toml"
15
+ - ".github/workflows/**"
16
+
17
+ # Run on all pull requests because these checks are required and will block merges otherwise
18
+ pull_request:
19
+
20
+ workflow_dispatch:
21
+
22
+ permissions:
23
+ contents: read
24
+
25
+ jobs:
26
+ static_analysis:
27
+ timeout-minutes: 2
28
+
29
+ runs-on: ubuntu-latest
30
+
31
+ steps:
32
+ - uses: actions/checkout@v5
33
+ - name: Install uv
34
+ uses: astral-sh/setup-uv@v6
35
+ with:
36
+ enable-cache: true
37
+ cache-dependency-glob: "uv.lock"
38
+
39
+ - name: Install dependencies
40
+ run: uv sync
41
+
42
+ - name: Check lockfile is up to date
43
+ run: |
44
+ if ! uv lock --check; then
45
+ echo "❌ Lockfile is out of date!"
46
+ echo "To update the lockfile, run 'uv lock'."
47
+ exit 1
48
+ fi
49
+ echo "✅ Lockfile is up to date"
50
+
51
+ - name: Lint
52
+ run: uv run ruff check src tests
53
+
54
+ - name: Check formatting
55
+ run: uv run ruff format --check src tests
56
+
57
+ - name: Type checking
58
+ run: uv run ty check src
@@ -0,0 +1,51 @@
1
+ name: Run tests
2
+
3
+ env:
4
+ # Enable colored output
5
+ PY_COLORS: 1
6
+
7
+ on:
8
+ push:
9
+ branches: ["main"]
10
+ paths:
11
+ - "src/**"
12
+ - "tests/**"
13
+ - "uv.lock"
14
+ - "pyproject.toml"
15
+ - ".github/workflows/**"
16
+
17
+ # Run on all pull requests because these checks are required and will block merges otherwise
18
+ pull_request:
19
+
20
+ workflow_dispatch:
21
+
22
+ permissions:
23
+ contents: read
24
+
25
+ jobs:
26
+ run_tests:
27
+ name: "Run tests: Python ${{ matrix.python-version }} on ${{ matrix.os }}"
28
+ runs-on: ${{ matrix.os }}
29
+ strategy:
30
+ matrix:
31
+ os: [ubuntu-latest, windows-latest]
32
+ python-version: ["3.10"]
33
+ fail-fast: false
34
+ timeout-minutes: 5
35
+
36
+ steps:
37
+ - uses: actions/checkout@v5
38
+
39
+ - name: Install uv
40
+ uses: astral-sh/setup-uv@v6
41
+ with:
42
+ enable-cache: true
43
+ cache-dependency-glob: "uv.lock"
44
+ python-version: ${{ matrix.python-version }}
45
+
46
+ - name: Install dependencies
47
+ # Run with frozen to use the current lockfile; static checks will determine if it needs updating
48
+ run: uv sync --frozen
49
+
50
+ - name: Run tests
51
+ run: uv run pytest
@@ -0,0 +1,24 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv/
11
+
12
+ # Log files
13
+ *.log
14
+
15
+ # Coverage reports
16
+ .coverage
17
+ htmlcov/
18
+ .pytest_cache/
19
+
20
+ # VSCode settings
21
+ .vscode/PythonImportHelper-v2-Completion.json
22
+
23
+ # Claude Code local settings (tool permissions, personal overrides — not shared)
24
+ .claude/settings.local.json
@@ -0,0 +1,41 @@
1
+ fail_fast: false
2
+
3
+ repos:
4
+ - repo: https://github.com/abravalheri/validate-pyproject
5
+ rev: v0.24.1
6
+ hooks:
7
+ - id: validate-pyproject
8
+
9
+ - repo: https://github.com/pre-commit/mirrors-prettier
10
+ rev: v3.1.0
11
+ hooks:
12
+ - id: prettier
13
+ types_or: [yaml, json]
14
+
15
+ - repo: https://github.com/astral-sh/ruff-pre-commit
16
+ # Ruff version.
17
+ rev: v0.15.12
18
+ hooks:
19
+ # Run the linter.
20
+ - id: ruff-check
21
+ args: [--fix, --exit-non-zero-on-fix]
22
+ # Run the formatter.
23
+ - id: ruff-format
24
+
25
+ - repo: local
26
+ hooks:
27
+ - id: ty
28
+ name: ty check
29
+ entry: uv run ty check
30
+ language: system
31
+ types: [python]
32
+ files: ^src/|^tests/
33
+ pass_filenames: false
34
+ require_serial: true
35
+
36
+ - repo: https://github.com/codespell-project/codespell
37
+ rev: v2.4.1
38
+ hooks:
39
+ - id: codespell # See pyproject.toml for args
40
+ additional_dependencies:
41
+ - tomli
@@ -0,0 +1,124 @@
1
+ # Contributing <!-- omit in toc -->
2
+
3
+ Thank you for your interest in contributing to `fastmcp-creds`! This document
4
+ provides guidelines for setting up a development environment, making changes,
5
+ and testing them locally before pushing to GitHub.
6
+
7
+ - [Development Setup](#development-setup)
8
+ - [Prerequisites](#prerequisites)
9
+ - [Clone and install](#clone-and-install)
10
+ - [Code Quality](#code-quality)
11
+ - [Enable automatic execution on git commit](#enable-automatic-execution-on-git-commit)
12
+ - [Manual execution](#manual-execution)
13
+ - [Testing](#testing)
14
+ - [Release Process](#release-process)
15
+ - [Testing a release first](#testing-a-release-first)
16
+ - [Building Packages Locally](#building-packages-locally)
17
+ - [Dependency Management](#dependency-management)
18
+
19
+ ## Development Setup
20
+
21
+ ### Prerequisites
22
+
23
+ - [Python 3.10](https://www.python.org/downloads) or later
24
+ - [uv](https://docs.astral.sh/uv/) — fast Python package manager
25
+
26
+ ### Clone and install
27
+
28
+ ```bash
29
+ git clone https://github.com/stephaneberle9/fastmcp-creds.git
30
+ cd fastmcp-creds
31
+ uv sync
32
+ ```
33
+
34
+ ## Code Quality
35
+
36
+ This project uses `pre-commit` hooks for static checks to maintain high code
37
+ quality standards:
38
+
39
+ | Hook | Purpose |
40
+ | ---- | ------- |
41
+ | `ruff-check` | Python linting (with auto-fix) |
42
+ | `ruff-format` | Python code formatting |
43
+
44
+ ### Enable automatic execution on git commit
45
+
46
+ ```bash
47
+ uv run pre-commit install
48
+ ```
49
+
50
+ ### Manual execution
51
+
52
+ ```bash
53
+ # Run all checks on all files
54
+ uv run pre-commit run --all-files
55
+
56
+ # Run individual tools
57
+ uv run ruff format src tests # Code formatting
58
+ uv run ruff check --fix src tests # Linting with auto-fix
59
+ uv run ty check src # Type checking
60
+ ```
61
+
62
+ ## Testing
63
+
64
+ ```bash
65
+ # Run all tests with coverage
66
+ uv run pytest
67
+
68
+ # Run a specific test file
69
+ uv run pytest tests/test_chain.py
70
+
71
+ # Run with verbose output
72
+ uv run pytest -v
73
+
74
+ # Show only a coverage summary (skip HTML report)
75
+ uv run pytest --cov-report=term-missing --no-cov-on-fail
76
+ ```
77
+
78
+ Coverage is collected from `src/fastmcp_creds` and an HTML report is written
79
+ to `htmlcov/`.
80
+
81
+ ## Release Process
82
+
83
+ 1. Ensure all changes are committed and the `main` branch is up to date.
84
+
85
+ 2. Create and push a version tag:
86
+
87
+ ```bash
88
+ git tag v0.1.0
89
+ git push origin v0.1.0
90
+ ```
91
+
92
+ 3. Create a GitHub release from the tag and add release notes. The
93
+ `publish.yml` workflow triggers automatically and uploads the package to
94
+ PyPI.
95
+
96
+ The package version is derived automatically from the git tag by
97
+ [uv-dynamic-versioning](https://github.com/nicoddemus/uv-dynamic-versioning).
98
+
99
+ ### Testing a release first
100
+
101
+ To publish to TestPyPI before a real release, trigger the `publish-test.yml`
102
+ workflow manually from the **Actions** tab on GitHub (select **Run workflow**).
103
+
104
+ ## Building Packages Locally
105
+
106
+ ```bash
107
+ uv build
108
+ ls dist/
109
+ ```
110
+
111
+ This produces both a wheel (`.whl`) and a source distribution (`.tar.gz`).
112
+
113
+ ## Dependency Management
114
+
115
+ Dependencies are pinned in `uv.lock` for reproducible installs. After adding or
116
+ changing dependencies in `pyproject.toml`:
117
+
118
+ ```bash
119
+ uv lock # update the lockfile
120
+ uv sync # sync your environment to the new lockfile
121
+ ```
122
+
123
+ > [!IMPORTANT]
124
+ > Always commit both `pyproject.toml` and `uv.lock` together.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stephan Eberle
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,265 @@
1
+ Metadata-Version: 2.5
2
+ Name: fastmcp-creds
3
+ Version: 0.1.0
4
+ Summary: Credential provider chain for FastMCP servers: keyring, environment variables, and HTTP Basic Auth header providers with unresolved placeholder detection.
5
+ Project-URL: Homepage, https://github.com/stephaneberle9/fastmcp-creds
6
+ Project-URL: Repository, https://github.com/stephaneberle9/fastmcp-creds
7
+ Project-URL: Issues, https://github.com/stephaneberle9/fastmcp-creds/issues
8
+ Author-email: Stephan Eberle <stephaneberle9@gmail.com>
9
+ License-File: LICENSE
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: credchain>=0.1.0
12
+ Requires-Dist: fastmcp<5,>=4.0.0
13
+ Provides-Extra: keyring
14
+ Requires-Dist: credchain[keyring]; extra == 'keyring'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # fastmcp-creds <!-- omit in toc -->
18
+
19
+ Pluggable credential management for FastMCP servers — supply API tokens or
20
+ username/password credentials for the backend services they connect to, from
21
+ environment variables, OS keychain, or HTTP headers, with automatic fallback
22
+ through a priority chain.
23
+
24
+ The framework-neutral parts — the chain and the environment and keychain
25
+ providers — live in [credchain](https://github.com/stephaneberle9/python-credchain)
26
+ and are re-exported here unchanged, so existing imports keep working. This
27
+ package adds the providers that read FastMCP's HTTP request headers.
28
+
29
+ - [Installation](#installation)
30
+ - [Usage](#usage)
31
+ - [Credential provider chain](#credential-provider-chain)
32
+ - [Environment variables](#environment-variables)
33
+ - [HTTP Basic Auth header](#http-basic-auth-header)
34
+ - [Custom HTTP headers](#custom-http-headers)
35
+ - [OS keychain](#os-keychain)
36
+ - [API Reference](#api-reference)
37
+ - [`CredentialsProvider`](#credentialsprovider)
38
+ - [`CredentialsProviderChain`](#credentialsproviderchain)
39
+ - [`EnvironmentCredentialsProvider`](#environmentcredentialsprovider)
40
+ - [`BasicAuthCredentialsProvider`](#basicauthcredentialsprovider)
41
+ - [`CustomHeaderCredentialsProvider`](#customheadercredentialsprovider)
42
+ - [`KeyringCredentialsProvider`](#keyringcredentialsprovider)
43
+ - [`BasicAuthError`](#basicautherror)
44
+ - [`is_unresolved_placeholder_value`](#is_unresolved_placeholder_value)
45
+ - [Contributing](#contributing)
46
+ - [License](#license)
47
+
48
+ ## Installation
49
+
50
+ ```bash
51
+ pip install fastmcp-creds
52
+ # or, with uv:
53
+ uv add fastmcp-creds
54
+ ```
55
+
56
+ The `KeyringCredentialsProvider` requires the optional `keyring` extra:
57
+
58
+ ```bash
59
+ pip install fastmcp-creds[keyring]
60
+ # or, with uv:
61
+ uv add fastmcp-creds[keyring]
62
+ ```
63
+
64
+ Requires Python 3.10+.
65
+
66
+ ## Usage
67
+
68
+ ### Credential provider chain
69
+
70
+ Compose providers in priority order. The chain tries each in sequence and
71
+ returns the first result where both username and password (or token) are
72
+ non-empty.
73
+
74
+ ```python
75
+ from fastmcp_creds import (
76
+ CredentialsProviderChain,
77
+ BasicAuthCredentialsProvider,
78
+ EnvironmentCredentialsProvider,
79
+ )
80
+ from fastmcp_creds.keyring import KeyringCredentialsProvider # optional dep
81
+
82
+ chain = CredentialsProviderChain([
83
+ KeyringCredentialsProvider("my-service"),
84
+ BasicAuthCredentialsProvider(),
85
+ EnvironmentCredentialsProvider("MY_USERNAME", "MY_PASSWORD"),
86
+ ])
87
+
88
+ username, password = chain.get_credentials()
89
+ ```
90
+
91
+ ### Environment variables
92
+
93
+ Read username and password from two environment variables:
94
+
95
+ ```python
96
+ from fastmcp_creds import EnvironmentCredentialsProvider
97
+
98
+ provider = EnvironmentCredentialsProvider("MY_USERNAME", "MY_PASSWORD")
99
+ username, password = provider.get_credentials()
100
+ ```
101
+
102
+ For a single token variable:
103
+
104
+ ```python
105
+ provider = EnvironmentCredentialsProvider.for_token("MY_API_TOKEN")
106
+ token, _ = provider.get_credentials()
107
+ ```
108
+
109
+ ### HTTP Basic Auth header
110
+
111
+ Decode credentials from the standard `Authorization: Basic <base64>` header.
112
+ Also accepts the non-compliant `Bearer Basic <base64>` variant emitted by MCP
113
+ Inspector, and raw base64 without a scheme prefix.
114
+
115
+ ```python
116
+ from fastmcp_creds import BasicAuthCredentialsProvider
117
+
118
+ provider = BasicAuthCredentialsProvider()
119
+ username, password = provider.get_credentials()
120
+ ```
121
+
122
+ Optionally check a custom header first before falling back to `Authorization`:
123
+
124
+ ```python
125
+ provider = BasicAuthCredentialsProvider(custom_header_name="X-My-Auth")
126
+ ```
127
+
128
+ ### Custom HTTP headers
129
+
130
+ Read credentials from two plain-text HTTP headers:
131
+
132
+ ```python
133
+ from fastmcp_creds import CustomHeaderCredentialsProvider
134
+
135
+ provider = CustomHeaderCredentialsProvider("X-Username", "X-Password")
136
+ username, password = provider.get_credentials()
137
+ ```
138
+
139
+ For a single token header:
140
+
141
+ ```python
142
+ provider = CustomHeaderCredentialsProvider.for_token("X-Api-Token")
143
+ token, _ = provider.get_credentials()
144
+ ```
145
+
146
+ ### OS keychain
147
+
148
+ Read credentials stored in the OS keychain via
149
+ [`keyring`](https://pypi.org/project/keyring/). Requires `fastmcp-creds[keyring]`.
150
+
151
+ ```python
152
+ from fastmcp_creds.keyring import KeyringCredentialsProvider
153
+
154
+ # Two-key mode: reads "username" and "password" entries under the service name
155
+ provider = KeyringCredentialsProvider("my-service")
156
+ username, password = provider.get_credentials()
157
+
158
+ # Token mode: reads a single "token" entry
159
+ provider = KeyringCredentialsProvider.for_token("my-service")
160
+ token, _ = provider.get_credentials()
161
+
162
+ # Custom key names
163
+ provider = KeyringCredentialsProvider("my-service", username_key="user", password_key="secret")
164
+ provider = KeyringCredentialsProvider.for_token("my-service", token_key="jwt")
165
+
166
+ # Custom lookup timeout in seconds (default 5), or None to wait indefinitely
167
+ provider = KeyringCredentialsProvider("my-service", timeout=10)
168
+ ```
169
+
170
+ The keychain lookup is bounded by a timeout, so a locked or unresponsive
171
+ keychain (e.g. one waiting for an unlock prompt in a headless session) cannot
172
+ stall server startup. On timeout, a warning is logged and `(None, None)` is
173
+ returned, so a `CredentialsProviderChain` falls through to its next provider.
174
+
175
+ ## API Reference
176
+
177
+ ### `CredentialsProvider`
178
+
179
+ `CredentialsProvider` is a `Protocol` — any object with a
180
+ `get_credentials() -> tuple[str | None, str | None]` method satisfies it.
181
+
182
+ ### `CredentialsProviderChain`
183
+
184
+ `CredentialsProviderChain(providers)` — tries each provider in order and
185
+ returns the first result where both values are non-empty. Returns
186
+ `(None, None)` if no provider succeeds.
187
+
188
+ | Method | Description |
189
+ | --- | --- |
190
+ | `get_credentials()` | Returns `tuple[str \| None, str \| None]` — `(username, password)` or `(token, token)` |
191
+
192
+ ### `EnvironmentCredentialsProvider`
193
+
194
+ `EnvironmentCredentialsProvider(username_env_var_name, password_env_var_name)`
195
+ — reads two environment variables.
196
+
197
+ `EnvironmentCredentialsProvider.for_token(token_env_var_name)` — reads a
198
+ single environment variable; returns it as both elements of the tuple.
199
+
200
+ | Method | Description |
201
+ | --- | --- |
202
+ | `get_credentials()` | Returns `tuple[str \| None, str \| None]` |
203
+
204
+ ### `BasicAuthCredentialsProvider`
205
+
206
+ `BasicAuthCredentialsProvider(custom_header_name=None)` — extracts credentials
207
+ from an HTTP Basic Auth header. Checks `custom_header_name` first if provided,
208
+ then falls back to the standard `Authorization` header. Silently ignores
209
+ unresolved MCP client placeholders (e.g. `${MY_VAR}`).
210
+
211
+ | Method | Description |
212
+ | --- | --- |
213
+ | `get_credentials()` | Returns `tuple[str \| None, str \| None]`; raises `BasicAuthError` on malformed headers |
214
+
215
+ ### `CustomHeaderCredentialsProvider`
216
+
217
+ `CustomHeaderCredentialsProvider(username_header_name, password_header_name)`
218
+ — reads credentials from two plain-text HTTP headers. Ignores unresolved
219
+ placeholders.
220
+
221
+ `CustomHeaderCredentialsProvider.for_token(token_header_name)` — reads a
222
+ single header; returns it as both elements of the tuple.
223
+
224
+ | Method | Description |
225
+ | --- | --- |
226
+ | `get_credentials()` | Returns `tuple[str \| None, str \| None]` |
227
+
228
+ ### `KeyringCredentialsProvider`
229
+
230
+ `KeyringCredentialsProvider(service, username_key="username", password_key="password", timeout=5.0)`
231
+ — reads credentials stored in the OS keychain under the given service name.
232
+ Returns `(None, None)` if the keychain does not answer within `timeout` seconds
233
+ (`None` waits indefinitely). Requires `fastmcp-creds[keyring]`.
234
+
235
+ `KeyringCredentialsProvider.for_token(service, token_key="token", timeout=5.0)`
236
+ — reads a single keychain entry.
237
+
238
+ | Method | Description |
239
+ | --- | --- |
240
+ | `get_credentials()` | Returns `tuple[str \| None, str \| None]` |
241
+
242
+ ### `BasicAuthError`
243
+
244
+ `BasicAuthError(message, header_name)` — raised by `BasicAuthCredentialsProvider`
245
+ when a header is present but malformed (bad base64, unsupported scheme, missing
246
+ colon separator, empty username or password).
247
+
248
+ | Field | Type | Description |
249
+ | --- | --- | --- |
250
+ | `header_name` | `str` | Name of the header that caused the error |
251
+
252
+ ### `is_unresolved_placeholder_value`
253
+
254
+ `is_unresolved_placeholder_value(value: str) -> bool` — returns `True` when a
255
+ header value is an unresolved MCP client placeholder such as `${MY_VAR}` or
256
+ `Basic ${MY_VAR}`. Used internally by `BasicAuthCredentialsProvider` and
257
+ `CustomHeaderCredentialsProvider`; exposed for custom provider implementations.
258
+
259
+ ## Contributing
260
+
261
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
262
+
263
+ ## License
264
+
265
+ MIT — see [LICENSE](LICENSE).