wakeshift
Let agents subscribe to things in your app. You define the events your app offers and emit them when they happen; Wakeshift wakes every agent subscribed to them. Clients that speak MCP Events, such as ChatGPT, subscribe natively; every other agent (Claude Code, Codex, Cursor and the rest) subscribes with a tool the SDK adds, and Wakeshift wakes it its own way. You write no subscribe tool and handle no harnesses.
Python 3.10 or later, on the official MCP Python SDK (mcp 2.3 or later). The same API as the Node and Go SDKs.
Install
pip install wakeshift
Or uv add wakeshift.
Quickstart: events
Register an app in the Wakeshift console (Build) and put its key in WAKESHIFT_KEY. The SDK talks to https://app.wakeshift.ai unless WAKESHIFT_CORE names another address.
import os
from mcp.server.mcpserver import MCPServer
from pydantic import BaseModel
from wakeshift import GuestAuth, Wakeshift
wake = Wakeshift(os.environ["WAKESHIFT_KEY"])
class Channel(BaseModel):
channel: str
class Message(BaseModel):
sender: str
text: str
message = wake.define_event(
"chat.message",
description="A new message in a channel",
args=Channel,
payload=Message,
text=lambda m, a: f"{m.sender} in #{a.channel}: {m.text}",
)
class Table(BaseModel):
table: str
class Turn(BaseModel):
hole: list[str]
to_call: int
# A personal event: each subscription belongs to its subscriber, and an
# occurrence goes to one of them, so it can carry their own private view.
turn = wake.define_event(
"game.turn",
description="It's your move at a table",
args=Table,
payload=Turn,
text=lambda t, a: f"Your move at {a.table}: {t.to_call} to call",
personal=True,
)
server = MCPServer("chat")
wake.attach(server) # define every event first
# Guests: MCP clients sign in with no account, each in its own workspace.
guests = GuestAuth(wake, "https://chat.example.com/mcp")
app = guests.wrap(server.streamable_http_app()) # run with uvicorn
# When something happens:
async def on_message(channel: str, sender: str, text: str) -> None:
await message.emit({"channel": channel}, {"sender": sender, "text": text})
async def on_turn(table: str, player: str, hole: list[str], to_call: int) -> None:
await turn.emit({"table": table}, {"hole": hole, "to_call": to_call}, to=player)
That's all. An agent subscribes to chat.message for eng, and each message in #eng wakes it with the text line and the payload, framed as information from your app, not as instructions from its user.
wake.define_event(name, description=, args=, payload=, text=, authorize=None, personal=False, delete_on_delivery=False)returns the event.argsandpayloadare pydantic models or JSON Schemas.argsis what a subscriber picks; its required string, integer and boolean fields route occurrences, so their values are lowercase IDs (a-z 0-9 . _ -), and an optional one filters on the payload field of the same name.text(payload, args)is the line the agent reads first.authorize(subscriber, args)(sync or async) decides who may subscribe. Withdelete_on_delivery=True, Wakeshift deletes each occurrence's text and payload once it's delivered.await event.emit(args, payload, to=None, id=None)checks the payload and sends it to everyone subscribed with those arguments; a personal event goes to the one subscribertonames.idmakes retries emit once.wake.attach(server, identify=None)attaches Wakeshift to your MCP server. Today it servesevents/list,events/subscribeandevents/unsubscribeto clients that speak MCP Events, and adds the toolssubscribe_events(for everyone else),unsubscribe_eventsandlist_subscriptions(for every client, so a person can always see and stop what they're subscribed to).identify(ctx)says who is behind a request, as anIdentity(user=…)with your ID for the person; by default, the guestGuestAuthsigned in. Each connection is answered for its own client, so serve over stateful Streamable HTTP (the default) or stdio.await event.subscribe(ctx, args, user=, wake_args=)subscribes the conversation calling one of your own tools, the waysubscribe_eventswould, so a tool like "join" sets up its wakes in one step.result(text)is your tool's text with what the agent needs added.await event.unsubscribe(ctx, args, user=, wake_args=)ends the calling conversation's subscription with those arguments, for a tool like "unwatch".await event.end(user, id)ends one of a user's subscriptions by its ID.
Sign-in
GuestAuth(wake, mcp_url)is a small OAuth server inside your app for MCP clients: dynamic client registration, authorize with PKCE, tokens, all signed with a key derived from your app key, so it keeps no state. Each registered client is a guest with its own workspace.guests.wrap(app)serves its endpoints and requires a token on/mcp; inside a tool,guest_of(ctx)is who's calling. Or callhandle(request)andauthenticate(request)from your own routes, or passverify_tokento the MCP SDK's own auth. Registration and token requests are rate-limited per address (IPv6 per /64); behind a proxy, setproxy_hops.grants=Truegives each guest a grant under your manifest's agent scope, checked at every refresh.- Your app identifies its own users, with
GuestAuthor its own sign-in, and acts for the agents in the workspaces it keeps.
Topics
Events are built on topics, which you can use directly: subscribe the conversation calling one of your tools, and publish to it.
from mcp.server.mcpserver import Context
from mcp.types import CallToolResult
from wakeshift import ConversationId, Harness, SessionKey
@server.tool()
async def watch_ci(
repo: str, ctx: Context, session_key: SessionKey = None, harness: Harness = None, conversation_id: ConversationId = None
) -> CallToolResult:
"""Wake me when CI finishes on a repo"""
call = wake.call(ctx, session=session_key, harness=harness, conversation=conversation_id)
sub = await wake.subscribe(call, f"ci/{repo.lower()}", replay="5m")
return sub.result(f"Watching CI on {repo}.")
async def on_ci(repo: str, conclusion: str, url: str) -> None:
await wake.publish(f"ci/{repo.lower()}", text=f"CI {conclusion} on {repo}", data={"url": url})
SessionKey, Harness and ConversationId add the optional arguments Wakeshift needs from the agent: the session key an earlier result gave it (a secret: don't log tool arguments that carry it), the program it runs in, and its own conversation ID. The other calls: unsubscribe, send (one subscription), topic_status, subscription_status, publish_url and revoke_publish_url, annotate (adds held messages to another tool's result), close_topic, end_subscription, unsubscribe_user, forget_user (deletes everything Wakeshift keeps for one of your users) and publish_manifest.
Errors
Calls raise WakeshiftError with the API's code (private_topic, bad_request…) and HTTP status; is_code(err, code) checks one. A core that doesn't answer is unreachable (status 0), and the message names the address it tried.
Keys
An app key is wst_ followed by 50 letters and digits: your app's Ed25519 private key, with a checksum, so a mistyped key fails when you create Wakeshift. It never goes over the wire: each request carries a fresh assertion signed with it, valid two minutes. A key goes to https://app.wakeshift.ai unless you pass core or set WAKESHIFT_CORE.
The key doesn't name your app. await wake.app() asks Wakeshift which app it is and returns its id and name; after that, wake.app_id is the app's ID (before it, reading wake.app_id raises).
Older keys, wst_test_<app>_<seed> and wst_live_<app>_<seed>, keep working. They name the app, so wake.app_id works at once. A wst_test_ key needs core or WAKESHIFT_CORE. wake.mode is "test" or "live" for an older key and None for a current one; it's deprecated.
Documentation
The guides and the full reference are at wakeshift.ai/docs: the quickstart, events, your users and the Python SDK. The same calls are in the TypeScript and Go SDKs.
Development
uv sync
uv run ruff check
uv run pyright
uv run pytest
The end-to-end tests run against a Wakeshift server, so they run in Wakeshift's own repository and are skipped here.
Contributing and security
This repository is generated from Wakeshift's main repository. Pull requests are welcome: we apply them there and publish the result. See CONTRIBUTING. Report a security problem privately to support@wakeshift.ai, not in an issue (SECURITY).
License
Apache License 2.0; see LICENSE.
Metadata
Release files for wakeshift 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| wakeshift-0.2.0.tar.gz | 45.4 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| wakeshift-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 93.8 kB
Release files / wakeshift-0.2.0.tar.gz
| Download URL | wakeshift-0.2.0.tar.gz |
|---|---|
| Size | 45.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
10c923809ba05eca285bad9880d0208894fac65d5052b05ffd57800aca80986f
|
|
BLAKE2b-256 checksum How to use checksums |
1d31f3071814386188a9baa79c2fd67b38a56c63a26c67da119eadcb5c3b84f3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logRelease files / wakeshift-0.2.0-py3-none-any.whl
| Download URL | wakeshift-0.2.0-py3-none-any.whl |
|---|---|
| Size | 48.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
944d60da50163b207e925e925bd8e6e71711a2bcddffcd20ac12a67594be0829
|
|
BLAKE2b-256 checksum How to use checksums |
74d20f7fd80461d74f3b2af698495d569845180bcca22ba3398b26f6cec65d5b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logSecurity for wakeshift 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for wakeshift-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.