Metadata-Version: 2.4
Name: levante-mcp-sdk
Version: 0.2.1
Summary: Levante OAuth provider for MCP servers (framework-agnostic core + adapters).
License: MIT
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27
Provides-Extra: fastmcp
Requires-Dist: fastmcp>=2.11.1; extra == 'fastmcp'
Provides-Extra: jwt
Requires-Dist: pyjwt[crypto]>=2.8; extra == 'jwt'
Provides-Extra: mcp
Requires-Dist: mcp>=1.25.0; extra == 'mcp'
Description-Content-Type: text/markdown

# levante-mcp-sdk

OAuth provider for [FastMCP](https://github.com/jlowin/fastmcp) servers deployed through
[Levante](https://platform.levanteapp.com). It wraps your MCP server with a `RemoteAuthProvider`
that validates Levante-issued tokens: it verifies the JWT locally against Levante's JWKS and
confirms the grant/membership via Levante's introspection endpoint
(`/api/v1/mcp/introspect`), with a short-lived in-memory cache.

> Part of the [`sdks-levante`](../) repository. The TypeScript SDK (`@levante/mcp-sdk`) will
> live in `packages/mcp-sdk/` of the same repo.

## Install

```bash
pip install levante-mcp-sdk                 # núcleo (solo cliente de introspección)
pip install "levante-mcp-sdk[mcp]"          # + adaptador SDK oficial de Anthropic (mcp)
pip install "levante-mcp-sdk[fastmcp]"      # + adaptador FastMCP v2 (standalone)
pip install "levante-mcp-sdk[jwt]"          # + verificación JWT local
```

Requires Python `>=3.11`. The core depends only on `httpx`; each framework is an optional extra.

## Uso con el SDK oficial (`mcp.server.fastmcp.FastMCP`)

```python
from mcp.server.auth.settings import AuthSettings
from mcp.server.fastmcp import FastMCP
from pydantic import AnyHttpUrl
from levante_mcp_sdk import LevanteConfig
from levante_mcp_sdk.integrations.mcp import LevanteTokenVerifier

cfg = LevanteConfig.from_env()
mcp = FastMCP(
    "my-mcp",
    token_verifier=LevanteTokenVerifier(cfg),
    auth=AuthSettings(
        issuer_url=AnyHttpUrl(cfg.issuer),
        resource_server_url=AnyHttpUrl(cfg.resource),
    ),
)
```

## Uso con FastMCP v2 (standalone)

```python
from fastmcp import FastMCP
from levante_mcp_sdk import LevanteAuthProvider

mcp = FastMCP(name="my-mcp", auth=LevanteAuthProvider.from_env())
```

`LevanteAuthProvider.from_env()` reads its configuration from environment variables. When the
MCP is deployed through Levante with a `levante.yaml`, these are injected automatically; you can
also set them manually for local development.

## Configuration (environment variables)

| Variable | Required | Description |
|---|---|---|
| `LEVANTE_MCP_BASE_URL` | yes | Public base URL of this MCP server (the `RemoteAuthProvider` base URL, **not** the Levante platform URL). |
| `LEVANTE_INTROSPECT_URL` | yes | Levante introspection endpoint, e.g. `https://platform.levanteapp.com/api/v1/mcp/introspect`. |
| `LEVANTE_API_KEY` | yes | A real Levante API key (`lv_sk_*`) the MCP uses to authenticate against `/introspect`. |
| `LEVANTE_ISSUER` | yes | OAuth issuer / authorization server, e.g. `https://platform.levanteapp.com`. |
| `LEVANTE_JWKS_URI` | yes | JWKS URI, e.g. `https://platform.levanteapp.com/.well-known/jwks.json`. |
| `LEVANTE_MCP_RESOURCE` | yes | Resource/audience for this MCP. |
| `LEVANTE_REQUIRE_ORG_MEMBERSHIP` | no (`true`) | If `true`, denies tokens whose user is no longer an org member. |
| `LEVANTE_FAIL_OPEN` | no (`false`) | If `true`, allows requests when introspection is unreachable. Leave `false` in production. |
| `LEVANTE_CACHE_TTL_SECONDS` | no (`45`) | TTL of the in-memory introspection cache. |

Example:

```bash
LEVANTE_MCP_BASE_URL=https://crm-conquer.example.run.app
LEVANTE_INTROSPECT_URL=https://platform.levanteapp.com/api/v1/mcp/introspect
LEVANTE_API_KEY=lv_sk_...
LEVANTE_ISSUER=https://platform.levanteapp.com
LEVANTE_JWKS_URI=https://platform.levanteapp.com/.well-known/jwks.json
LEVANTE_MCP_RESOURCE=https://crm-conquer.example.run.app
LEVANTE_REQUIRE_ORG_MEMBERSHIP=true
LEVANTE_FAIL_OPEN=false
LEVANTE_CACHE_TTL_SECONDS=45
```

`LEVANTE_API_KEY` must be a real key registered in Levante; it cannot be an arbitrary string.

## How it works

1. **JWT verification (local, offline):** the bearer token is verified against `LEVANTE_JWKS_URI`
   with the expected issuer and audience.
2. **Introspection (cached):** the token is checked against `LEVANTE_INTROSPECT_URL`, which
   returns whether the grant is active and whether the user is still an org member.
3. **Membership gate:** with `LEVANTE_REQUIRE_ORG_MEMBERSHIP=true`, a non-member is denied even
   if the JWT is otherwise valid.
4. On success, Levante identity is attached to the token claims: `levante_user_id`,
   `levante_org_id`, `levante_mcp_slug`.

Revocations may take up to `LEVANTE_CACHE_TTL_SECONDS` to take effect on each running instance.

## Build

```bash
pip install hatch
hatch build
```

Publishing to PyPI is automated via `.github/workflows/publish-python.yml` on GitHub release.

## License

MIT
