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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ jobs:
uses: astral-sh/setup-uv@v4

- name: Install just
uses: extractions/setup-just@v2
run: |
mkdir -p "$HOME/.local/bin"
curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh \
| bash -s -- --tag 1.50.0 --to "$HOME/.local/bin"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- name: Sync dependencies
run: uv sync --all-extras --all-packages
Expand Down
18 changes: 15 additions & 3 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ jobs:
uses: astral-sh/setup-uv@v4

- name: Install just
uses: extractions/setup-just@v2
run: |
mkdir -p "$HOME/.local/bin"
curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh \
| bash -s -- --tag 1.50.0 --to "$HOME/.local/bin"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- name: Validate commit messages
run: just validate-commits
Expand All @@ -48,7 +52,11 @@ jobs:
uses: astral-sh/setup-uv@v4

- name: Install just
uses: extractions/setup-just@v2
run: |
mkdir -p "$HOME/.local/bin"
curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh \
| bash -s -- --tag 1.50.0 --to "$HOME/.local/bin"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- name: Install dependencies
run: uv sync --all-extras
Expand Down Expand Up @@ -79,7 +87,11 @@ jobs:
uses: astral-sh/setup-uv@v4

- name: Install just
uses: extractions/setup-just@v2
run: |
mkdir -p "$HOME/.local/bin"
curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh \
| bash -s -- --tag 1.50.0 --to "$HOME/.local/bin"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- name: Install dependencies
run: uv sync --all-extras
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
- '*-keycardai-starlette'
- '*-keycardai-mcp'
- '*-keycardai-mcp-fastmcp'
- '*-keycardai-fastmcp'
- '*-keycardai-agents'

jobs:
Expand Down
42 changes: 42 additions & 0 deletions packages/fastmcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# keycardai-fastmcp

FastMCP integration for Keycard OAuth: protect FastMCP servers with Keycard
authentication and run delegated OAuth 2.0 token exchange (RFC 8693) for
downstream APIs.

This is the canonical home for the integration; `keycardai-mcp-fastmcp` is
preserved as a deprecation bridge for callers still on the old name.

## Installation

```bash
pip install keycardai-fastmcp
```

## Quick Start

```python
from fastmcp import FastMCP, Context
from keycardai.fastmcp import AuthProvider

auth_provider = AuthProvider(
zone_id="abc1234",
mcp_server_name="My Server",
mcp_base_url="http://localhost:8000",
)

mcp = FastMCP("My Server", auth=auth_provider.get_remote_auth_provider())

@mcp.tool()
@auth_provider.grant("https://api.example.com")
async def call_external_api(ctx: Context, query: str):
keycardai = await ctx.get_state("keycardai")
token = keycardai.access("https://api.example.com").access_token
return f"Results for {query} (token starts with {token[:8]})"
```

## Migration from `keycardai-mcp-fastmcp`

The old package keeps working: `from keycardai.mcp.integrations.fastmcp import AuthProvider`
emits a `DeprecationWarning` pointing here and returns the same class. Migrate
when convenient.
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Create a GitHub App in your organization (or personal account) to enable delegat
2. Click **New GitHub App**
3. Configure the app with the permissions your MCP server needs (e.g., **Repository** access, **User** profile read)
4. Generate a **Client Secret**
5. Note down the **Client ID** and **Client Secret** you'll use these to configure the credential provider in Keycard
5. Note down the **Client ID** and **Client Secret**; you'll use these to configure the credential provider in Keycard

### 5. Configure a Credential Provider in Keycard

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import httpx
from fastmcp import Context, FastMCP

from keycardai.mcp.integrations.fastmcp import AccessContext, AuthProvider, ClientSecret
from keycardai.fastmcp import AccessContext, AuthProvider, ClientSecret

# Configure Keycard authentication with client credentials for delegated access
# Set KEYCARD_ZONE_ID (or KEYCARD_ZONE_URL) and client credentials from console.keycard.ai
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ description = "GitHub API integration with Keycard delegated access using the @g
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"keycardai-mcp-fastmcp",
"keycardai-fastmcp",
"fastmcp>=3.0.0",
"httpx>=0.27.0,<1.0.0",
]

[tool.uv.sources]
keycardai-mcp-fastmcp = { path = "../../", editable = true }
keycardai-fastmcp = { path = "../../", editable = true }

[project.scripts]
delegated-access-server = "main:main"
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ A minimal example demonstrating how to add Keycard authentication to a FastMCP s

## Why Keycard?

Keycard lets you securely connect your AI IDE or agent to external resources. It provides OAuth-based authentication for your MCP server plus auditabilityso you know who accessed what.
Keycard lets you securely connect your AI IDE or agent to external resources. It provides OAuth-based authentication for your MCP server plus auditability, so you know who accessed what.

## Prerequisites

Before running this example, set up Keycard:

1. **Sign up** at [keycard.ai](https://keycard.ai)
2. **Create a zone** this is your authentication boundary
3. **Configure an identity provider** (Google, Microsoft, etc.) this is how your users will sign in
4. **Create an MCP resource** with URL `http://localhost:8000/` — this registers your server with Keycard
2. **Create a zone**: this is your authentication boundary
3. **Configure an identity provider** (Google, Microsoft, etc.): this is how your users will sign in
4. **Create an MCP resource** with URL `http://localhost:8000/` to register your server with Keycard

Once configured, get your **zone ID** from the Keycard console. See [MCP Server Setup](https://docs.keycard.ai/build-with-keycard/mcp-server) for detailed instructions.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from fastmcp import FastMCP

from keycardai.mcp.integrations.fastmcp import AuthProvider
from keycardai.fastmcp import AuthProvider

# Configure Keycard authentication
# Get your zone_id from console.keycard.ai
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ description = "A minimal FastMCP server with Keycard authentication"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"keycardai-mcp-fastmcp",
"keycardai-fastmcp",
"fastmcp>=3.0.0",
]

[tool.uv.sources]
keycardai-mcp-fastmcp = { path = "../../", editable = true }
keycardai-fastmcp = { path = "../../", editable = true }

[project.scripts]
hello-world-server = "main:main"
125 changes: 125 additions & 0 deletions packages/fastmcp/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
[project]
name = "keycardai-fastmcp"
dynamic = ["version"]
description = "FastMCP integration for Keycard OAuth client with automated token exchange and authentication"
readme = "README.md"
requires-python = ">=3.10"
license = { text = "MIT" }
authors = [{ name = "Keycard", email = "support@keycard.ai" }]
dependencies = [
"pydantic>=2.11.7",
"pydantic-settings>=2.7.1",
"httpx>=0.27.2",
"keycardai-oauth>=0.7.0",
"fastmcp>=3.0.0",
"keycardai-mcp>=0.15.0",
]
keywords = ["fastmcp", "mcp", "model-context-protocol", "oauth", "token-exchange", "authentication", "keycard"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Operating System :: OS Independent",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Security",
"Topic :: Internet :: WWW/HTTP :: Session",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
"License :: OSI Approved :: MIT License",
]

[project.optional-dependencies]
test = [
"pytest>=8.4.1",
"pytest-asyncio>=1.1.0",
]

[project.urls]
Homepage = "https://github.com/keycardai/python-sdk"
Repository = "https://github.com/keycardai/python-sdk"
Documentation = "https://docs.keycardai.com"
Issues = "https://github.com/keycardai/python-sdk/issues"

[build-system]
requires = ["hatchling", "uv-dynamic-versioning"]
build-backend = "hatchling.build"

[tool.hatch.version]
source = "uv-dynamic-versioning"

[tool.uv-dynamic-versioning]
vcs = "git"
pattern = "(?P<base>\\d+\\.\\d+\\.\\d+)-keycardai-fastmcp"
style = "pep440"

[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
publish-url = "https://test.pypi.org/legacy/"
explicit = true

[tool.hatch.build.targets.wheel]
packages = ["src/keycardai"]

[tool.ruff]
line-length = 88
target-version = "py310"

[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
]
ignore = [
"E501", # line too long, we'll handle case by case
]
isort = { combine-as-imports = true, known-first-party = ["keycardai"] }

[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["T20"]

[tool.mypy]
strict = true
disallow_incomplete_defs = false
disallow_untyped_defs = false
disallow_untyped_calls = false

[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false

[tool.coverage.run]
source = ["tests", "src/keycardai"]

[tool.coverage.report]
show_missing = true
exclude_also = [
"pragma: no cover",
"if TYPE_CHECKING:",
"@abc.abstractmethod",
"raise NotImplementedError",
]

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra -q"

[tool.commitizen]
name = "cz_customize"
version = "0.1.0"
tag_format = "${version}-keycardai-fastmcp"
ignored_tag_formats = ["${version}-*"]
update_changelog_on_bump = true
bump_message = "bump: keycardai-fastmcp $current_version → $new_version"
major_version_zero = true

[tool.commitizen.customize]
changelog_pattern = "^(feat|fix|refactor|perf|test|build|ci|revert)\\(keycardai-fastmcp\\)(!)?:"
Loading
Loading