Add fastmcp-slim for client-only installs (#4122)

* Add fastmcp-client workspace package

* Fix client package static checks

* Document client-only package

* Harden fastmcp-client package split

* Preserve forwarded headers in full package

* Switch to fastmcp-slim package

* Fix fastmcp-slim release edges

* Match pydantic-style slim layout

* Polish fastmcp-slim packaging
This commit is contained in:
Jeremiah Lowin 2026-05-11 17:13:21 -04:00 committed by GitHub
commit bb4894d215
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
298 changed files with 1259 additions and 438 deletions

View file

@ -125,7 +125,7 @@ jobs:
<evidence_standards>
Every claim in your response must be grounded in evidence you can cite:
- **Code references**: Always include file path and line number (e.g., `src/fastmcp/client/client.py:142`). Never say "the client code does X" without pointing to where.
- **Code references**: Always include file path and line number (e.g., `fastmcp_slim/fastmcp/client/client.py:142`). Never say "the client code does X" without pointing to where.
- **Bug confirmation**: If you say a bug is real, show the specific code path that produces it. If you ran a test, include the command and output.
- **Related items**: When citing a related issue or PR, explain specifically why it's related — not just that it exists.
- **Confidence**: If you're uncertain about a finding, say so. "I don't know" or "I couldn't confirm this" is better than a speculative diagnosis. Only report findings you would confidently defend.

View file

@ -116,7 +116,7 @@ jobs:
- auth: Authentication is the main concern (Bearer, JWT, OAuth, WorkOS)
- openapi: OpenAPI integration/parsing is the primary topic
- http: HTTP transport or networking is the main issue
- contrib: Specifically about community contributions in src/contrib/
- contrib: Specifically about community contributions in fastmcp_slim/fastmcp/contrib/
- tests: Issues primarily about testing infrastructure, CI/CD workflows, or test coverage
- security: Apply ONLY when the issue/PR addresses an exploitable vulnerability or hardens against one. Examples: SSRF, LFI, path traversal, injection, auth bypass allowing unauthorized access, scope escalation, open redirects. Do NOT apply for ordinary auth bugs (wrong scopes returned, token refresh logic, OAuth flow correctness) unless an attacker could exploit the bug to bypass access controls or escalate privileges. The key question: "Could a malicious actor exploit this?" If the answer is just "it breaks for legitimate users," that's a bug, not a security issue.

View file

@ -0,0 +1,30 @@
name: Publish fastmcp-slim to PyPI
on:
release:
types: [published]
workflow_dispatch:
permissions:
contents: read
id-token: write
jobs:
pypi-publish:
name: Upload fastmcp-slim to PyPI
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Build fastmcp-slim
run: uv build --package fastmcp-slim
- name: Publish fastmcp-slim to PyPI
run: uv publish -v dist/fastmcp_slim-*.tar.gz dist/fastmcp_slim-*.whl

87
.github/workflows/publish-fastmcp.yml vendored Normal file
View file

@ -0,0 +1,87 @@
name: Publish fastmcp to PyPI
on:
workflow_run:
workflows: ["Publish fastmcp-slim to PyPI"]
types: [completed]
workflow_dispatch:
permissions:
contents: read
id-token: write
jobs:
pypi-publish:
name: Upload fastmcp to PyPI
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'release')
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Build fastmcp
run: uv build --package fastmcp
- name: Verify matching fastmcp-slim is published
run: |
SLIM_VERSION=$(python - <<'PY'
import email.parser
import re
import zipfile
from pathlib import Path
wheel = next(Path("dist").glob("fastmcp-*.whl"))
metadata_name = next(
name for name in zipfile.ZipFile(wheel).namelist()
if name.endswith(".dist-info/METADATA")
)
metadata = email.parser.Parser().parsestr(
zipfile.ZipFile(wheel).read(metadata_name).decode()
)
for value in metadata.get_all("Requires-Dist", []):
requirement, _, marker = value.partition(";")
if marker.strip():
continue
match = re.fullmatch(
r"fastmcp-slim(?:\[[^\]]+\])?==([^;\s]+)",
requirement.strip(),
)
if match:
print(match.group(1))
break
else:
raise RuntimeError("Could not find the base fastmcp-slim dependency")
PY
)
for attempt in {1..12}; do
if python - "$SLIM_VERSION" <<'PY'
import json
import sys
import urllib.request
version = sys.argv[1]
url = f"https://pypi.org/pypi/fastmcp-slim/{version}/json"
with urllib.request.urlopen(url, timeout=30) as response:
json.load(response)
PY
then
exit 0
fi
echo "fastmcp-slim ${SLIM_VERSION} is not available on PyPI yet; retrying (${attempt}/12)."
sleep 10
done
echo "fastmcp-slim ${SLIM_VERSION} is not available on PyPI; refusing to publish fastmcp." >&2
exit 1
- name: Publish fastmcp to PyPI
run: uv publish -v dist/fastmcp-*.tar.gz dist/fastmcp-*.whl

View file

@ -1,26 +0,0 @@
name: Publish FastMCP to PyPI
on:
release:
types: [published]
workflow_dispatch:
jobs:
pypi-publish:
name: Upload to PyPI
runs-on: ubuntu-latest
permissions:
id-token: write # For PyPI's trusted publishing
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: "Install uv"
uses: astral-sh/setup-uv@v7
- name: Build
run: uv build
- name: Publish to PyPi
run: uv publish -v dist/*

View file

@ -4,21 +4,21 @@ on:
push:
branches: ["main"]
paths:
- "src/fastmcp/utilities/json_schema_type.py"
- "src/fastmcp/utilities/json_schema.py"
- "src/fastmcp/utilities/openapi/**"
- "src/fastmcp/server/providers/openapi/**"
- "src/fastmcp/client/mixins/tools.py"
- "fastmcp_slim/fastmcp/utilities/json_schema_type.py"
- "fastmcp_slim/fastmcp/utilities/json_schema.py"
- "fastmcp_slim/fastmcp/utilities/openapi/**"
- "fastmcp_slim/fastmcp/server/providers/openapi/**"
- "fastmcp_slim/fastmcp/client/mixins/tools.py"
- "tests/utilities/json_schema_type/test_real_world_schemas.py"
- ".github/workflows/run-schema-crash-test.yml"
pull_request:
paths:
- "src/fastmcp/utilities/json_schema_type.py"
- "src/fastmcp/utilities/json_schema.py"
- "src/fastmcp/utilities/openapi/**"
- "src/fastmcp/server/providers/openapi/**"
- "src/fastmcp/client/mixins/tools.py"
- "fastmcp_slim/fastmcp/utilities/json_schema_type.py"
- "fastmcp_slim/fastmcp/utilities/json_schema.py"
- "fastmcp_slim/fastmcp/utilities/openapi/**"
- "fastmcp_slim/fastmcp/server/providers/openapi/**"
- "fastmcp_slim/fastmcp/client/mixins/tools.py"
- "tests/utilities/json_schema_type/test_real_world_schemas.py"
- ".github/workflows/run-schema-crash-test.yml"

View file

@ -7,10 +7,10 @@ on:
push:
branches: ["main"]
paths:
- "src/**"
- "fastmcp_slim/**"
- "tests/**"
- "uv.lock"
- "pyproject.toml"
- "uv.lock"
- ".github/workflows/**"
# run on all pull requests because these checks are required and will block merges otherwise

View file

@ -7,10 +7,10 @@ on:
push:
branches: ["main"]
paths:
- "src/**"
- "fastmcp_slim/**"
- "tests/**"
- "uv.lock"
- "pyproject.toml"
- "uv.lock"
- ".github/workflows/**"
# run on all pull requests because these checks are required and will block merges otherwise
@ -117,3 +117,102 @@ jobs:
FASTMCP_GITHUB_TOKEN: ${{ secrets.FASTMCP_GITHUB_TOKEN }}
FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID: ${{ secrets.FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID }}
FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET: ${{ secrets.FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET }}
package_install_smoke:
name: "Package install smoke"
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- name: Setup uv
uses: ./.github/actions/setup-uv
with:
resolution: locked
- name: Build package wheels
run: uv build --all-packages --wheel --out-dir /tmp/fastmcp-dist
- name: Install bare slim wheel
run: |
uv venv /tmp/fastmcp-slim-bare-smoke
SLIM_WHEEL=$(ls /tmp/fastmcp-dist/fastmcp_slim-*.whl)
uv pip install --python /tmp/fastmcp-slim-bare-smoke/bin/python "$SLIM_WHEEL"
/tmp/fastmcp-slim-bare-smoke/bin/python - <<'PY'
from importlib.metadata import entry_points
import fastmcp
import fastmcp.settings
assert not any(ep.name == "fastmcp" for ep in entry_points(group="console_scripts"))
try:
fastmcp.FastMCP
except ImportError as exc:
assert "fastmcp-slim[server]" in str(exc)
else:
raise AssertionError("bare fastmcp-slim unexpectedly imported FastMCP")
PY
- name: Install client slim wheel
run: |
uv venv /tmp/fastmcp-slim-client-smoke
SLIM_WHEEL=$(ls /tmp/fastmcp-dist/fastmcp_slim-*.whl)
uv pip install --python /tmp/fastmcp-slim-client-smoke/bin/python "${SLIM_WHEEL}[client]"
/tmp/fastmcp-slim-client-smoke/bin/python - <<'PY'
from importlib.metadata import entry_points
from fastmcp import Client
from fastmcp.client.transports import StdioTransport, StreamableHttpTransport
from fastmcp.mcp_config import MCPConfig
assert not any(ep.name == "fastmcp" for ep in entry_points(group="console_scripts"))
assert Client("https://example.com/mcp")
assert StreamableHttpTransport("https://example.com/mcp")
assert StdioTransport(command="uvx", args=["demo"])
assert MCPConfig.from_dict({"mcpServers": {"demo": {"url": "https://example.com/mcp"}}})
try:
from fastmcp import FastMCP
except ImportError as exc:
assert "fastmcp-slim[server]" in str(exc)
else:
raise AssertionError(f"client-only slim unexpectedly imported {FastMCP!r}")
PY
- name: Install server slim wheel
run: |
uv venv /tmp/fastmcp-slim-server-smoke
SLIM_WHEEL=$(ls /tmp/fastmcp-dist/fastmcp_slim-*.whl)
uv pip install --python /tmp/fastmcp-slim-server-smoke/bin/python "${SLIM_WHEEL}[server]"
/tmp/fastmcp-slim-server-smoke/bin/python - <<'PY'
from fastmcp import FastMCP
mcp = FastMCP("smoke")
assert mcp.name == "smoke"
PY
- name: Install full package from matching local wheels
run: |
uv venv /tmp/fastmcp-full-smoke
FULL_WHEEL=$(ls /tmp/fastmcp-dist/fastmcp-*.whl)
uv pip install --python /tmp/fastmcp-full-smoke/bin/python --find-links /tmp/fastmcp-dist "$FULL_WHEEL"
/tmp/fastmcp-full-smoke/bin/python - <<'PY'
from importlib.metadata import entry_points
from fastmcp import Client, FastMCP
from fastmcp.client.client import CallToolResult
from fastmcp.exceptions import ToolError
assert any(
ep.name == "fastmcp" and ep.value == "fastmcp.cli:app"
for ep in entry_points(group="console_scripts")
)
assert Client("https://example.com/mcp")
assert FastMCP("smoke").name == "smoke"
assert CallToolResult is not None
assert ToolError is not None
PY

View file

@ -7,10 +7,10 @@ on:
push:
branches: ["main"]
paths:
- "src/**"
- "fastmcp_slim/**"
- "tests/**"
- "uv.lock"
- "pyproject.toml"
- "uv.lock"
- ".github/workflows/**"
schedule:

View file

@ -7,8 +7,8 @@ on:
push:
branches: ["main"]
paths:
- "src/fastmcp/utilities/mcp_server_config/**"
- "!src/fastmcp/utilities/mcp_server_config/v1/schema.json"
- "fastmcp_slim/fastmcp/utilities/mcp_server_config/**"
- "!fastmcp_slim/fastmcp/utilities/mcp_server_config/v1/schema.json"
workflow_dispatch:
permissions:
@ -47,7 +47,7 @@ jobs:
from fastmcp.utilities.mcp_server_config import generate_schema
generate_schema('docs/public/schemas/fastmcp.json/latest.json')
generate_schema('docs/public/schemas/fastmcp.json/v1.json')
generate_schema('src/fastmcp/utilities/mcp_server_config/v1/schema.json')
generate_schema('fastmcp_slim/fastmcp/utilities/mcp_server_config/v1/schema.json')
"
- name: Create Pull Request
@ -59,7 +59,7 @@ jobs:
body: |
This PR updates the fastmcp.json schema files to match the current source code.
The schema is automatically generated from `src/fastmcp/utilities/mcp_server_config/` to ensure consistency.
The schema is automatically generated from `fastmcp_slim/fastmcp/utilities/mcp_server_config/` to ensure consistency.
**Note:** This PR is fully automated and will update itself with any subsequent changes to the schema, or close automatically if the schema becomes up-to-date through other means.

View file

@ -7,7 +7,7 @@ on:
push:
branches: ["main"]
paths:
- "src/**"
- "fastmcp_slim/**"
- "pyproject.toml"
workflow_dispatch: