Metadata-Version: 2.4
Name: levante-mcp-sdk
Version: 0.1.1
Summary: Levante OAuth provider for FastMCP servers.
License: MIT
Requires-Python: >=3.11
Requires-Dist: fastmcp>=2.11.1
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.0
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
```

Requires Python `>=3.11` and `fastmcp>=2.11.1`.

## Usage

```python
from fastmcp import FastMCP
from fastmcp.server.dependencies import get_access_token
from levante_mcp_sdk import LevanteAuthProvider

mcp = FastMCP(name="crm-conquer", auth=LevanteAuthProvider.from_env())


@mcp.tool
def whoami() -> dict:
    token = get_access_token()
    if token is None:
        return {"error": "not_authenticated"}
    return {
        "user_id": token.claims.get("levante_user_id"),
        "org_id": token.claims.get("levante_org_id"),
        "mcp_slug": token.claims.get("levante_mcp_slug"),
    }
```

`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
