A FastMCP server that enables AI agents to schedule notifications and reminders via Upstash QStash and NTFY.
- Instant Push Notifications - Send immediate notifications with rich formatting, actions, and attachments
- One-off Scheduled Notifications - Schedule notifications for a specific future time using ISO 8601, date/time/timezone, or delay format
- Recurring Cron Notifications - Create persistent schedules using standard cron syntax
| Tool | Description |
|---|---|
health |
Check server configuration and environment status |
send_push_notification |
Send an immediate push notification |
schedule_notification |
Schedule a one-off notification for a future time |
schedule_cron_notification |
Schedule recurring notifications using cron syntax |
list_scheduled_notifications |
List all recurring cron schedules (optionally filter by topic) |
pause_schedule |
Temporarily pause a cron schedule |
resume_schedule |
Resume a paused cron schedule |
delete_schedule |
Permanently delete a cron schedule |
- Python 3.13+
- uv package manager
- Docker Desktop 4.50+ (for Docker Sandboxes development)
- Upstash QStash account and token
- NTFY topic for receiving notifications (passed as
notification_topicparameter to each tool)
git clone https://github.com/your-org/cronty-mcp.git
cd cronty-mcp
uv synccp .env.example .envEdit .env with your credentials:
QSTASH_TOKEN=your_qstash_token_here
# For local development without auth:
AUTH_DISABLED=true
# Or for production with auth:
# JWT_SECRET=your_secret_here # Generate with: openssl rand -base64 48Note: The NTFY topic is now specified per-request via the notification_topic parameter on each tool call, enabling multi-user and multi-tenant deployments.
uv run fastmcp run server.pyFor development with the MCP Inspector:
uv run fastmcp dev server.pyCronty MCP supports bearer token authentication using JWT tokens signed with HS512.
Generate a secure secret (minimum 64 characters):
# macOS/Linux
openssl rand -base64 48
# Or using Python
python -c "import secrets; print(secrets.token_urlsafe(48))"Add the secret to your .env:
JWT_SECRET=your_generated_secret_hereIssue tokens for users via CLI:
uv run python -m cronty token issue --email user@example.comWith custom expiration:
uv run python -m cronty token issue --email user@example.com --expires-in 30dSupported duration formats: 30d, 12h, 1y, 365d
For local development, disable auth by setting:
AUTH_DISABLED=trueConfigure your AI agent to connect to the local MCP server.
Note: These configurations run the server locally with AUTH_DISABLED=true for development.
Add to your project's .mcp.json:
{
"mcpServers": {
"cronty-mcp": {
"command": "uv",
"args": ["run", "fastmcp", "run", "server.py"]
}
}
}Or use the CLI:
claude mcp add cronty-mcp -- uv run fastmcp run server.pyAdd to your Claude Desktop configuration file:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"cronty-mcp": {
"command": "uv",
"args": ["run", "fastmcp", "run", "server.py"],
"cwd": "/path/to/cronty-mcp"
}
}
}Add to your Cursor MCP configuration (.cursor/mcp.json in your project or global settings):
{
"mcpServers": {
"cronty-mcp": {
"command": "uv",
"args": ["run", "fastmcp", "run", "server.py"],
"cwd": "/path/to/cronty-mcp"
}
}
}Add to your VS Code settings (.vscode/mcp.json or user settings):
{
"mcpServers": {
"cronty-mcp": {
"command": "uv",
"args": ["run", "fastmcp", "run", "server.py"],
"cwd": "/path/to/cronty-mcp"
}
}
}Add to your Windsurf MCP configuration (~/.windsurf/mcp.json or project-level):
{
"mcpServers": {
"cronty-mcp": {
"command": "uv",
"args": ["run", "fastmcp", "run", "server.py"],
"cwd": "/path/to/cronty-mcp"
}
}
}codex mcp add cronty-mcp -- uv run fastmcp run server.pygemini mcp add cronty-mcp -- uv run fastmcp run server.pyWhen deployed to FastMCP Cloud, you can connect to your server using bearer token authentication.
Replace your-hostname with your actual FastMCP Cloud hostname (e.g., your-app-name.fastmcp.app).
Note: Bearer token authentication is a temporary solution for clients that don't yet support OAuth 2.0 Dynamic Client Registration (DCR). OAuth with DCR support via WorkOS/Authkit is planned as the preferred authentication method.
Set your bearer token as an environment variable:
export CRONTY_TOKEN="your-token-here"Before connecting, issue a token for each user:
uv run python -m cronty token issue --email user@example.comUsers will need this token to authenticate with the cloud-deployed server.
In the Obsidian MCP plugin settings, add a new server:
| Field | Value |
|---|---|
| Server name | Cronty |
| Server URL | https://your-hostname.fastmcp.app/mcp |
| Authentication | Bearer Token |
| Token | (paste token from CLI) |
Using CLI:
claude mcp add --transport http cronty-mcp https://your-hostname.fastmcp.app/mcp \
--header "Authorization: Bearer ${CRONTY_TOKEN}"Or add to .mcp.json:
{
"mcpServers": {
"cronty-mcp": {
"type": "http",
"url": "https://your-hostname.fastmcp.app/mcp",
"headers": {
"Authorization": "Bearer ${CRONTY_TOKEN}"
}
}
}
}Claude Desktop requires the mcp-remote wrapper to add custom headers. Add to claude_desktop_config.json:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"cronty-mcp": {
"command": "npx",
"args": [
"mcp-remote@latest",
"https://your-hostname.fastmcp.app/mcp",
"--header",
"Authorization: Bearer YOUR_TOKEN"
]
}
}
}Replace YOUR_TOKEN with your actual token from the CLI.
Edit ~/.codex/config.toml:
[mcp_servers.cronty-mcp]
url = "https://your-hostname.fastmcp.app/mcp"
bearer_token_env_var = "CRONTY_TOKEN"Then set the environment variable before running Codex.
Using CLI:
gemini mcp add cronty-mcp https://your-hostname.fastmcp.app/mcp \
--transport http \
--header "Authorization: Bearer ${CRONTY_TOKEN}"Or edit settings.json:
{
"mcpServers": {
"cronty-mcp": {
"httpUrl": "https://your-hostname.fastmcp.app/mcp",
"headers": {
"Authorization": "Bearer ${CRONTY_TOKEN}"
}
}
}
}Add to .cursor/mcp.json:
{
"mcpServers": {
"cronty-mcp": {
"url": "https://your-hostname.fastmcp.app/mcp",
"headers": {
"Authorization": "Bearer ${env:CRONTY_TOKEN}"
}
}
}
}Note: Cursor uses ${env:VAR} syntax for environment variables.
Add to .vscode/mcp.json:
{
"inputs": [
{
"type": "promptString",
"id": "cronty-token",
"description": "Cronty MCP Bearer Token",
"password": true
}
],
"servers": {
"cronty-mcp": {
"type": "http",
"url": "https://your-hostname.fastmcp.app/mcp",
"headers": {
"Authorization": "Bearer ${input:cronty-token}"
}
}
}
}VS Code will securely prompt for your token on first use.
import asyncio
from fastmcp import Client
from fastmcp.client.auth import BearerAuth
client = Client(
"https://your-hostname.fastmcp.app/mcp",
auth=BearerAuth("your-token-here")
)
async def main():
async with client:
await client.ping()
tools = await client.list_tools()
result = await client.call_tool(
"send_push_notification",
{"message": "Hello from Cronty!"}
)
print(result)
asyncio.run(main())import os
from openai import OpenAI
client = OpenAI()
resp = client.responses.create(
model="gpt-4.1",
tools=[
{
"type": "mcp",
"server_label": "cronty-mcp",
"server_url": "https://your-hostname.fastmcp.app/mcp",
"headers": {
"Authorization": f"Bearer {os.environ['CRONTY_TOKEN']}"
},
"require_approval": "never",
},
],
input="Send me a test notification",
)OAuth 2.0 with Dynamic Client Registration (DCR) support via WorkOS/Authkit is planned. This will enable:
- Automatic token refresh
- Secure authorization flows
- No manual token management
Clients with native OAuth DCR support (Claude Code, VS Code, Cursor) will be able to authenticate without bearer tokens once implemented.
Run evaluations against your MCP server using Claude to verify tool effectiveness.
Add your Anthropic API key to .env:
# In .env
ANTHROPIC_API_KEY=your_api_key_hereNote: Requires an Anthropic API key from console.anthropic.com. Claude Max subscription does not include API access.
Create an XML file with question-answer pairs (see evaluation.xml for examples):
<evaluation>
<qa_pair>
<question>Use the health tool. Is the server healthy? Answer: Yes or No.</question>
<answer>Yes</answer>
</qa_pair>
</evaluation>uv run python .claude/skills/fastmcp-builder/scripts/evaluation.py \
-c "uv run fastmcp run server.py" \
evaluation.xmlAgainst HTTP server:
uv run python .claude/skills/fastmcp-builder/scripts/evaluation.py \
-t http \
-u https://your-hostname.fastmcp.app/mcp \
evaluation.xmlWith custom model and output:
uv run python .claude/skills/fastmcp-builder/scripts/evaluation.py \
-c "uv run fastmcp run server.py" \
-m claude-sonnet-4-20250514 \
-o report.md \
evaluation.xmlIf you don't want evaluation dependencies in your project:
cd .claude/skills/fastmcp-builder/scripts
uv sync
uv run python evaluation.py \
-c "uv run fastmcp run server.py" \
--cwd ../../../.. \
../../../../evaluation.xml- Questions must be READ-ONLY, INDEPENDENT, NON-DESTRUCTIVE, IDEMPOTENT
- Answers must be single, verifiable values (not lists or objects)
- Answers must be STABLE (won't change over time)
- Create challenging questions that require multiple tool calls
See .claude/skills/fastmcp-builder/reference/evaluation.md for the complete guide.
uv syncuv run pytestuv run ruff check .
uv run ruff check . --fix
uv run ruff format .For local development, use stdio transport with auth disabled. Set in .env:
AUTH_DISABLED=trueThe repo includes .mcp.json for local testing:
{
"mcpServers": {
"cronty-mcp": {
"command": "uv",
"args": ["run", "fastmcp", "run", "server.py"]
}
}
}Then run Claude Code from this directory - it will automatically detect the MCP server.
To test against FastMCP Cloud deployment:
-
Set your token:
export CRONTY_TOKEN="your-token-here"
-
Update
.mcp.jsonto use HTTP transport:{ "mcpServers": { "cronty-mcp": { "type": "http", "url": "https://your-hostname.fastmcp.app/mcp", "headers": { "Authorization": "Bearer ${CRONTY_TOKEN}" } } } } -
Run Claude Code with the env var set.
Run Claude Code in an isolated Docker container with all dependencies pre-installed.
docker build -t cronty-dev .The .env file is not mounted in the container for security. Set variables in your shell:
# Option 1: Source from .env file
set -a; source .env; set +a
# Option 2: Add to ~/.zshrc or ~/.bashrc for persistence
export QSTASH_TOKEN=your_qstash_token_here
export ANTHROPIC_API_KEY=your_api_key_here # Required for running evaluations
export JWT_SECRET=your_jwt_secret_here # Required if AUTH_DISABLED is not set# Development mode (auth disabled)
docker sandbox run \
-e QSTASH_TOKEN=$QSTASH_TOKEN \
-e AUTH_DISABLED=true \
--template cronty-dev claude
# Continue a previous conversation
docker sandbox run \
-e QSTASH_TOKEN=$QSTASH_TOKEN \
-e AUTH_DISABLED=true \
--template cronty-dev claude -c
# With a direct prompt
docker sandbox run \
-e QSTASH_TOKEN=$QSTASH_TOKEN \
-e AUTH_DISABLED=true \
--template cronty-dev claude "Run the tests"
# With auth enabled and evaluation support
docker sandbox run \
-e QSTASH_TOKEN=$QSTASH_TOKEN \
-e JWT_SECRET=$JWT_SECRET \
-e ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \
--template cronty-dev claudeGlobal Claude settings (~/.claude/settings.json) are not available inside the sandbox due to security restrictions. To use custom settings (hooks, permissions, preferences), create a local settings file in the project:
# Create local settings file
cp ~/.claude/settings.json .claude/settings.local.jsonThe .claude/settings.local.json file is mounted with the project and will be used by Claude Code inside the sandbox.
All uv commands work inside the sandbox:
uv run pytest # Run tests
uv run fastmcp dev server.py # Start dev server with MCP Inspector
uv run ruff check . # Lint code
uv add some-package # Add dependenciesThe Docker Sandbox template includes:
- Claude Code with automatic credential handling
- Python 3.13 with uv package manager
- All project dependencies pre-installed
- Docker CLI, GitHub CLI, Git, Node.js, Go
- Non-root
agentuser with sudo privileges
MIT