nevzheng opened a new pull request, #12531:
URL: https://github.com/apache/gravitino/pull/12531
### What changes were proposed in this pull request?
Give the MCP server an OAuth `client_credentials` service-identity path
alongside the existing `--token`, so a long-running MCP session no longer goes
401 forever once the pasted Bearer expires.
Hop-2 `Authorization` precedence:
```mermaid
flowchart TD
tool["tool call needs Gravitino"];
hasHeader{"incoming HTTP Authorization"};
hasToken{"static token configured"};
hasOauth{"OAuth client complete"};
forward["httpx.AsyncClient frozen hop-1 header"];
token["httpx.AsyncClient frozen static token"];
oauth["httpx.AsyncClient + RefreshableBearerAuth"];
anon["httpx.AsyncClient no Authorization"];
tool --> hasHeader;
hasHeader -->|yes| forward;
hasHeader -->|no| hasToken;
hasToken -->|yes wins| token;
hasToken -->|no| hasOauth;
hasOauth -->|yes| oauth;
hasOauth -->|no| anon;
```
Service OAuth is an `auth=` hook (`RefreshableBearerAuth` on `httpx-auth`)
on the existing `httpx.AsyncClient`. Tokens are fetched with a form POST
(`client_secret_post`: `grant_type`, `client_id`, `client_secret`, optional
`scope`), matching the Java/Python clients. The token is cached with 60s
early-expiry skew; if the IdP omits `expires_in`, expiry comes from the JWT
`exp` claim. One retry after Gravitino HTTP 401. Gravitino remains the
authenticator/authorizer; MCP only attaches `Authorization`. No identity
provider is added.
New flags / env: `--oauth-token-endpoint`
(`GRAVITINO_OAUTH_TOKEN_ENDPOINT`), `--oauth-client-id`
(`GRAVITINO_OAUTH_CLIENT_ID`), `--oauth-client-secret`
(`GRAVITINO_OAUTH_CLIENT_SECRET`), optional `--oauth-scope`
(`GRAVITINO_OAUTH_SCOPE`). The three required flags must be set together.
`--token` / `GRAVITINO_TOKEN` still works and overrides OAuth
client-credentials.
### Why are the changes needed?
A `--token` Bearer is an already-issued access token. MCP froze it at
process start. After its TTL, Gravitino stays `401` until a human pastes a new
token and restarts. Gravitino already validates OAuth JWTs and the Java/Python
clients already do `client_credentials`; MCP was the only client in this flow
that could not fetch or refresh a service token, so long-running sessions (e.g.
Cursor) broke on token expiry with no self-service recovery.
Fix: #12530
### Does this PR introduce _any_ user-facing change?
Yes.
1. New flags / env: `--oauth-token-endpoint`
(`GRAVITINO_OAUTH_TOKEN_ENDPOINT`), `--oauth-client-id`
(`GRAVITINO_OAUTH_CLIENT_ID`), `--oauth-client-secret`
(`GRAVITINO_OAUTH_CLIENT_SECRET`), `--oauth-scope` (`GRAVITINO_OAUTH_SCOPE`).
2. `--token` / `GRAVITINO_TOKEN` still works and overrides OAuth
client-credentials.
3. Docs: `docs/gravitino-mcp-server.md` — Cursor `mcp.json` `env` example,
service-identity OAuth section, JWT `principalFields` / grants note, and audit
attribution for the service path.
No new property keys under `gravitino.*`. No Helm chart change (this repo
ships no MCP chart).
### How was this patch tested?
- `cd mcp-server && env -u GRAVITINO_TOKEN uv run python -m unittest
tests.unit.test_oauth tests.unit.test_auth_flow -v` — 41 tests, OK.
- `env -u GRAVITINO_TOKEN uv run pytest
tests/integration/test_oauth_refresh_e2e.py -v` — 5 passed (fetch + form body +
Bearer, cache reuse, stale `expires_in=1` vs 60s skew, 401 retry, `--token`
skips IdP).
- `uv run python -m pylint mcp_server/core/oauth.py
mcp_server/core/context.py mcp_server/client/factory.py
mcp_server/client/plain/plain_rest_client_operation.py tests/unit/test_oauth.py
tests/unit/tools/mock_operation.py` — 10.00/10.
The process-level IT is the insert point that exists today
(`dev/run_authz_integration_test.sh` already runs `pytest tests/integration`).
Default PR Gradle (`unittest discover`) does not collect the pytest e2e. CI
workflow `mcp-integration-test.yml` is `workflow_dispatch` only.
### Compliance
- Full Apache License headers on all new/changed files; no Datastrato header.
- `httpx-auth` is MIT (ASF Category A). The `mcp-server` LICENSE/NOTICE
carry only the ASF boilerplate and do not enumerate third-party Python deps,
matching existing deps (`fastmcp`, `fakeredis`); no LICENSE/NOTICE change
required.
- No `gravitino.datastrato.*` keys, no enterprise license gate.
Related to: #12530
Sent from 🤖 Cursor (cloud agent)
Made with [Cursor](https://cursor.com)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]