Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
Empty file added .changes/unreleased/.gitkeep
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
kind: added
body: |
`client.actions.mcp.create_connection_session_token(connection_name, identifier, *, expiry=None, access_level=None)` mints a session token for a connection's own MCP server (`<environment URL>/mcp/v3/connections/<connection name>`), which serves all of that connection's tools without an MCP configuration; `access_level="READ_ONLY"` limits the token to read-only tools. The URL path is case-sensitive, so connect with the connection's name exactly as stored. `client.mcp.create_connection_session_token` takes the same arguments and returns the raw `(response, call)` tuple. [Reference](https://github.com/scalekit-inc/scalekit-sdk-python/blob/main/AGENTKIT.md#mcp-mcpclient)

```python
from datetime import timedelta

resp = client.actions.mcp.create_connection_session_token(
"gmail",
"user_123",
expiry=timedelta(minutes=15),
access_level="READ_ONLY",
)
headers = {"Authorization": f"Bearer {resp.token}"}
```
time: '2026-10-09T15:35:35.140631+00:00'
custom:
Breaking: 'No'
Ticket: SK-2195
Migration: ''
28 changes: 27 additions & 1 deletion AGENTKIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -829,7 +829,7 @@ The `ActionClient` also exposes `list_configs`, `create_config`, `update_config`

### `ActionMcp` helper

Access via `client.connect.mcp` / `client.actions.mcp`. Requires `McpClient` to be initialized on the parent `ScalekitClient`. Methods include `list_configs`, `create_config` (builds `McpConfig` from `name` / `description` / mappings), `update_config`, `delete_config`, `ensure_instance`, `update_instance`, `get_instance`, `list_instances`, `delete_instance`, and `get_instance_auth_state`, returning parsed wrapper types instead of raw gRPC tuples.
Access via `client.connect.mcp` / `client.actions.mcp`. Requires `McpClient` to be initialized on the parent `ScalekitClient`. Methods include `list_configs`, `create_config` (builds `McpConfig` from `name` / `description` / mappings), `update_config`, `delete_config`, `ensure_instance`, `update_instance`, `get_instance`, `list_instances`, `delete_instance`, `get_instance_auth_state`, and `create_connection_session_token`, returning parsed wrapper types instead of raw gRPC tuples.


## MCP (`McpClient`)
Expand Down Expand Up @@ -942,3 +942,29 @@ Returns authorization state for connectors used by the instance; optional fresh
</dd></dl>
</details>

<details><summary><code>client.mcp.<a href="https://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/mcp.py">create_connection_session_token</a>(connection_name, identifier, *, expiry?, access_level?) -> CreateMcpSessionTokenResponse</code></summary>
<dl><dd>

### 📝 Description

Mints a short-lived session token for one connection's MCP server (`<environment_url>/mcp/v3/connections/<connection_name>`), which serves every tool of that connection. The token works only on that server. `expiry` (a `timedelta`, 60 seconds to 24 hours, default 1 hour) and `access_level` (`"FULL"` or `"READ_ONLY"`) are keyword-only. `"READ_ONLY"` limits the token to tools annotated read-only. `connection_name` is matched without regard to case; the token is issued for the connection's MCP server URL built from its stored name, so connect to that URL using the stored name exactly as it appears (the URL is case-sensitive), e.g. copied from the dashboard. Raises `ScalekitNotFoundException` when no active connection has that name, and `ScalekitBadRequestException` when `identifier`, `expiry` or `access_level` is invalid, the connection is not an AgentKit connection, or, depending on the environment's configuration, `identifier` has no active connected account on the connection (otherwise a token is minted and the account is reported as not connected when tools are called).

### 🔌 Usage

```python
from datetime import timedelta

response, _ = scalekit_client.mcp.create_connection_session_token(
"gmail",
"user_123",
expiry=timedelta(minutes=15),
access_level="READ_ONLY",
)
headers = {"Authorization": f"Bearer {response.token}"}
```

`client.connect.mcp.create_connection_session_token(...)` / `client.actions.mcp.create_connection_session_token(...)` take the same arguments and return a parsed `CreateMcpSessionTokenResponse` with `token` and `expires_at` (a naive `datetime` in UTC).

</dd></dl>
</details>

516 changes: 516 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

19 changes: 9 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,12 @@ make test # unittest discover
make generate # Regenerate from proto (needs buf)
```

## Release

Package: `scalekit-sdk-python` on PyPI. Workflow: `.github/workflows/release.yml`.

1. Bump `scalekit/_version.py`. Use a minor or patch bump. `setup.py` reads this file.
2. Review the unreleased changes. If proto or generated API files changed, extra generation steps apply. Those steps are not documented yet. Do not invent them. Ask before you regenerate.
3. Merge the release branch to `main`.
4. Create a git tag that matches the version (`v2.17.0` for `2.17.0`). Draft a GitHub Release for that tag.
5. Publishing does not start on its own. Open the Actions run for the release workflow. Any peer can approve it. The `release` environment gates deploy.
6. After approval, the workflow publishes to PyPI.
## Release notes and releases

- Package: `scalekit-sdk-python` on PyPI. Workflow: `.github/workflows/release.yml`.
- Every change that users can notice adds a release-notes file under `.changes/unreleased/`.
- Never hand-edit the version (`scalekit/_version.py`) or `CHANGELOG.md` sections: release
tooling bumps the version and renders the CHANGELOG section from those files.
- Publishing a GitHub Release for a merged version starts the release workflow. It waits for
an approval on the `release` environment from someone other than the person who started
it, then publishes to PyPI.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Python versions](https://img.shields.io/pypi/pyversions/scalekit-sdk-python.svg?style=flat-square)](https://pypi.org/project/scalekit-sdk-python/)

<a href="https://scalekit.com" target="_blank" rel="noopener noreferrer">Scalekit</a> is the **auth stack for AI apps** - from human authentication to agent authorization. Build secure AI products faster with authentication for humans (SSO, passwordless, full-stack auth) and agents (MCP/APIs, delegated actions), all unified on one platform. This Python SDK enables both traditional B2B authentication and cutting-edge agentic workflows.
<a href="https://scalekit.com" target="_blank" rel="noopener noreferrer">Scalekit</a> provides auth and actions on behalf of users, with 500+ connectors and 20,000+ tools. Build secure AI products faster with authentication for humans (SSO, passwordless, full-stack auth) and agents (MCP/APIs, delegated actions), all unified on one platform. This Python SDK enables both traditional B2B authentication and cutting-edge agentic workflows.

## 🤖 Agent-First Features

Expand Down
75 changes: 75 additions & 0 deletions scalekit/actions/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1431,6 +1431,81 @@ def create_session_token(
)
return CreateMcpSessionTokenResponse.from_proto(result_tuple[0])

def create_connection_session_token(
self,
connection_name: str,
identifier: str,
*,
expiry: timedelta | None = None,
access_level: str | None = None,
) -> CreateMcpSessionTokenResponse:
"""Create a short-lived session token for a connection's MCP server.

A connection's MCP server serves every tool of one connection at
``<environment_url>/mcp/v3/connections/<connection_name>``. Pass the
token as a ``Bearer`` token in the ``Authorization`` header when calling
that URL. The token works only on that server; use
``create_session_token`` to mint a token for an MCP configuration.

Args:
connection_name: Name of the connection whose MCP server the token
grants access to, e.g. ``"gmail"``. Matched without regard to
case. The token is issued for the connection's MCP server URL
built from its stored name; connect to that URL using the
stored name exactly as it appears (the URL is case-sensitive),
e.g. copied from the dashboard.
identifier: End-user identifier whose connected account on this
connection the token acts as, e.g. ``"user_123"``.
1 to 255 characters.
expiry: Requested lifetime for the token. The server accepts
60 seconds to 24 hours and applies 1 hour when omitted.
Fractions of a second are dropped.
access_level: Tools the token can use. ``"READ_ONLY"`` limits it to
tools annotated read-only: other tools are left out of the tool
list and refused when called. ``"FULL"``, or omitting it,
exposes every tool of the connection. Values are case-sensitive.

Returns:
CreateMcpSessionTokenResponse: Contains:

- ``token`` (``str``) — opaque bearer token string.
- ``expires_at`` (``datetime``) — naive datetime, in UTC, when the
token expires.

Raises:
ValueError: If ``connection_name`` or ``identifier`` is blank.
ScalekitNotFoundException: No active connection has this name.
ScalekitBadRequestException: ``identifier``, ``expiry`` or
``access_level`` is invalid, the connection is not an
AgentKit connection, or, depending on the environment's
configuration, ``identifier`` has no active connected account
on the connection (otherwise a token is minted and the account
is reported as not connected when tools are called).

Example::

from datetime import timedelta

resp = client.actions.mcp.create_connection_session_token(
"gmail",
"user_123",
expiry=timedelta(minutes=15),
access_level="READ_ONLY",
)
headers = {"Authorization": f"Bearer {resp.token}"}
"""
if not connection_name:
raise ValueError("connection_name is required")
if not identifier:
raise ValueError("identifier is required")
result_tuple = self._client().create_connection_session_token(
connection_name,
identifier,
expiry=expiry,
access_level=access_level,
)
return CreateMcpSessionTokenResponse.from_proto(result_tuple[0])


class ActionProviders:
"""Typed action layer over ProvidersClient for custom provider CRUD.
Expand Down
74 changes: 74 additions & 0 deletions scalekit/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,80 @@ def create_session_token(
mcp_config_id=mcp_config_id,
identifier=identifier,
)
return self._mint_session_token(request, expiry, access_level)

def create_connection_session_token(
self,
connection_name: str,
identifier: str,
*,
expiry: timedelta | None = None,
access_level: str | None = None,
) -> CreateMcpSessionTokenResponse:
"""Create a short-lived session token for a connection's MCP server.

A connection's MCP server serves every tool of one connection at
``<environment_url>/mcp/v3/connections/<connection_name>``. The token
works only on that server; use ``create_session_token`` to mint a token
for an MCP configuration instead.

Args:
connection_name: Name of the connection whose MCP server the token
grants access to, e.g. ``"gmail"``. Matched without regard to
case. The token is issued for the connection's MCP server URL
built from its stored name; connect to that URL using the
stored name exactly as it appears (the URL is case-sensitive),
e.g. copied from the dashboard.
identifier: End-user identifier whose connected account on this
connection the token acts as, e.g. ``"user_123"``.
1 to 255 characters.
expiry: Lifetime of the token. The server accepts 60 seconds to
24 hours and applies 1 hour when omitted. Fractions of a second
are dropped.
access_level: Tools the token can use. ``"READ_ONLY"`` limits it to
tools annotated read-only: other tools are left out of the tool
list and refused when called. ``"FULL"``, or omitting it,
exposes every tool of the connection. Values are case-sensitive.

Returns:
CreateMcpSessionTokenResponse: ``token`` (str) and ``expires_at``
(Timestamp). Like the other methods of this client, the call
returns the ``(response, call)`` tuple.

Raises:
ScalekitNotFoundException: No active connection has this name.
ScalekitBadRequestException: ``identifier``, ``expiry`` or
``access_level`` is invalid, the connection is not an
AgentKit connection, or, depending on the environment's
configuration, ``identifier`` has no active connected account
on the connection (otherwise a token is minted and the account
is reported as not connected when tools are called).

Example::

from datetime import timedelta

response, _ = client.mcp.create_connection_session_token(
"gmail",
"user_123",
expiry=timedelta(minutes=15),
access_level="READ_ONLY",
)
headers = {"Authorization": f"Bearer {response.token}"}
"""
request = CreateMcpSessionTokenRequest(
key_id=connection_name,
identifier=identifier,
)
return self._mint_session_token(request, expiry, access_level)

def _mint_session_token(
self,
request: CreateMcpSessionTokenRequest,
expiry: timedelta | None,
access_level: str | None,
) -> CreateMcpSessionTokenResponse:
"""Set the optional fields shared by both session-token targets and send the request."""
if access_level is not None:
request.access_level = access_level
if expiry is not None:
Expand Down
25 changes: 25 additions & 0 deletions tests/test_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from faker import Faker
from basetest import BaseTest

from scalekit.common.exceptions import ScalekitNotFoundException

from scalekit.v1.mcp.mcp_pb2 import (
Mcp,
McpConfig,
Expand Down Expand Up @@ -299,3 +301,26 @@ def test_create_session_token_with_custom_expiry(self):
finally:
if created_config_id:
self.scalekit_client.mcp.delete_config(config_id=created_config_id)

def test_create_connection_session_token(self):
"""create_connection_session_token mints a token for a connection's MCP server."""
now = datetime.now(tz=timezone.utc)
token_response = self.scalekit_client.mcp.create_connection_session_token(
"GMAIL",
self.test_connected_account_identifier,
expiry=timedelta(minutes=10),
access_level="READ_ONLY",
)
self.assertEqual(token_response[1].code().name, "OK")
self.assertTrue(token_response[0].token, "Expected a non-empty token string")
expires_at = token_response[0].expires_at.ToDatetime(tzinfo=timezone.utc)
self.assertGreater(expires_at, now + timedelta(minutes=5))
self.assertLess(expires_at, now + timedelta(minutes=15))

def test_create_connection_session_token_unknown_connection(self):
"""An unknown connection name raises ScalekitNotFoundException."""
with self.assertRaises(ScalekitNotFoundException):
self.scalekit_client.mcp.create_connection_session_token(
f"py-test-missing-{uuid.uuid4().hex[:8]}",
self.test_user_identifier,
)
Loading
Loading