mirror of
https://github.com/jlengrand/ghost-mcp.git
synced 2026-03-10 08:21:19 +00:00
🎉 Initial commit
Implemented the first version of Ghost MCP
This commit is contained in:
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
ghost-admin-api.md
|
||||
mcp-python-sdk.md
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.12
|
||||
122
README.md
Normal file
122
README.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# Ghost MCP Server
|
||||
|
||||
A Model Context Protocol (MCP) server for interacting with Ghost CMS through LLM interfaces like Claude. This server provides secure and comprehensive access to your Ghost blog, leveraging JWT authentication and a rich set of MCP tools for managing posts, users, members, tiers, offers, and newsletters.
|
||||
|
||||
## Features
|
||||
|
||||
- Secure JWT Authentication for Ghost Admin API requests
|
||||
- Comprehensive entity access including posts, users, members, tiers, offers, and newsletters
|
||||
- Advanced search functionality with both fuzzy and exact matching options
|
||||
- Detailed, human-readable output for Ghost entities
|
||||
- Robust error handling using custom `GhostError` exceptions
|
||||
- Integrated logging support via MCP context for enhanced troubleshooting
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone git@github.com/mfydev/ghost-mcp.git
|
||||
cd ghost-mcp
|
||||
|
||||
# Create virtual environment and install
|
||||
uv venv
|
||||
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
||||
uv pip install -e .
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python ≥ 3.12
|
||||
- Running Ghost instance with Admin API access (v5.x+ recommended)
|
||||
- Node.js (for testing with MCP Inspector)
|
||||
|
||||
## Usage
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
GHOST_API_URL=https://yourblog.com # Your Ghost Admin API URL
|
||||
GHOST_STAFF_API_KEY=your_staff_api_key # Your Ghost Staff API key
|
||||
```
|
||||
|
||||
### Usage with Claude Desktop
|
||||
To use this with Claude Desktop, add the following to your `claude_desktop_config.json`:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"ghost": {
|
||||
"command": "/Users/username/.local/bin/uv",
|
||||
"args": [
|
||||
"--directory",
|
||||
"/path/to/ghost-mcp",
|
||||
"run",
|
||||
"src/main.py"
|
||||
],
|
||||
"env": {
|
||||
"GHOST_API_URL": "your_ghost_api_url",
|
||||
"GHOST_STAFF_API_KEY": "your_staff_api_key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Testing with MCP Inspector
|
||||
|
||||
```bash
|
||||
GHOST_API_URL=your_ghost_api_url GHOST_STAFF_API_KEY=your_staff_api_key npx @modelcontextprotocol/inspector uv --directory Desktop/mcp/ghost run src/main.py
|
||||
```
|
||||
|
||||
## Available Tools
|
||||
|
||||
### Posts Management
|
||||
- `list_posts`: List blog posts with pagination (supports both text and JSON formats)
|
||||
- `read_post`: Retrieve full content of a specific post in HTML or plaintext formats
|
||||
- `search_posts_by_title`: Search for posts by title using exact or fuzzy matching
|
||||
|
||||
### Users Management
|
||||
- `list_users`: List all users with detailed role information
|
||||
- `read_user`: Get comprehensive details of a specific user
|
||||
|
||||
### Members Management
|
||||
- `list_members`: List members with subscription and newsletter details
|
||||
- `read_member`: Retrieve detailed information for a specific member, including subscriptions
|
||||
|
||||
### Tiers Management
|
||||
- `list_tiers`: List all available membership tiers
|
||||
- `read_tier`: Retrieve detailed information about a specific tier, including benefits and pricing
|
||||
|
||||
### Offers Management
|
||||
- `list_offers`: List promotional offers with relevant details
|
||||
- `read_offer`: Get detailed information on a specific offer
|
||||
|
||||
### Newsletters Management
|
||||
- `list_newsletters`: List all newsletters associated with the blog
|
||||
- `read_newsletter`: Retrieve detailed settings and information for a specific newsletter
|
||||
|
||||
## Available Resources
|
||||
|
||||
All resources follow the URI pattern: `[type]://[id]`
|
||||
|
||||
- `user://{user_id}`: User profiles and roles
|
||||
- `member://{member_id}`: Member details and subscriptions
|
||||
- `tier://{tier_id}`: Tier configurations
|
||||
- `offer://{offer_id}`: Offer details
|
||||
- `newsletter://{newsletter_id}`: Newsletter settings
|
||||
- `post://{post_id}`: Post content and metadata
|
||||
- `blog://info`: General blog information
|
||||
|
||||
## Error Handling
|
||||
|
||||
Ghost MCP Server employs a custom `GhostError` exception to handle API communication errors and processing issues. This ensures clear and descriptive error messages to assist with troubleshooting.
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork repository
|
||||
2. Create feature branch
|
||||
3. Commit changes
|
||||
4. Create pull request
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
22
pyproject.toml
Normal file
22
pyproject.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[project]
|
||||
name = "ghost-mcp"
|
||||
version = "0.1.0"
|
||||
description = "Ghost blog integration MCP server"
|
||||
authors = [
|
||||
{ name = "Fanyang Meng" }
|
||||
]
|
||||
dependencies = [
|
||||
"httpx",
|
||||
"pyjwt",
|
||||
"mcp[cli]>=1.2.1"
|
||||
]
|
||||
requires-python = ">=3.12"
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/ghost_mcp"]
|
||||
|
||||
[tool.hatch.metadata]
|
||||
allow-direct-references = true
|
||||
3
src/ghost_mcp/__init__.py
Normal file
3
src/ghost_mcp/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .server import create_server
|
||||
|
||||
__all__ = ['create_server']
|
||||
103
src/ghost_mcp/api.py
Normal file
103
src/ghost_mcp/api.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""Ghost API interaction utilities."""
|
||||
|
||||
import datetime
|
||||
import httpx
|
||||
import jwt
|
||||
from typing import Dict, Any
|
||||
from mcp.server.fastmcp import Context
|
||||
|
||||
from .config import API_URL
|
||||
from .exceptions import GhostError
|
||||
|
||||
async def generate_token(staff_api_key: str, audience: str = "/admin/") -> str:
|
||||
"""Generate a JWT token for Ghost Admin API authentication.
|
||||
|
||||
Args:
|
||||
staff_api_key: API key in 'id:secret' format
|
||||
audience: Token audience (default: "/admin/")
|
||||
|
||||
Returns:
|
||||
JWT token string
|
||||
|
||||
Raises:
|
||||
ValueError: If staff_api_key is not in correct format
|
||||
"""
|
||||
try:
|
||||
key_id, secret = staff_api_key.split(":")
|
||||
except ValueError:
|
||||
raise ValueError("STAFF_API_KEY must be in the format 'id:secret'")
|
||||
|
||||
secret_bytes = bytes.fromhex(secret)
|
||||
now = datetime.datetime.now(datetime.UTC)
|
||||
exp = now + datetime.timedelta(minutes=5)
|
||||
|
||||
payload = {
|
||||
"iat": now,
|
||||
"exp": exp,
|
||||
"aud": audience
|
||||
}
|
||||
|
||||
token = jwt.encode(payload, secret_bytes, algorithm="HS256", headers={"kid": key_id})
|
||||
|
||||
if isinstance(token, bytes):
|
||||
token = token.decode("utf-8")
|
||||
|
||||
return token
|
||||
|
||||
async def get_auth_headers(staff_api_key: str) -> Dict[str, str]:
|
||||
"""Get authenticated headers for Ghost API requests.
|
||||
|
||||
Args:
|
||||
staff_api_key: API key in 'id:secret' format
|
||||
|
||||
Returns:
|
||||
Dictionary of request headers
|
||||
"""
|
||||
token = await generate_token(staff_api_key)
|
||||
return {
|
||||
"Authorization": f"Ghost {token}",
|
||||
"Accept-Version": "v5"
|
||||
}
|
||||
|
||||
async def make_ghost_request(
|
||||
endpoint: str,
|
||||
headers: Dict[str, str],
|
||||
ctx: Context = None,
|
||||
is_resource: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""Make an authenticated request to the Ghost API.
|
||||
|
||||
Args:
|
||||
endpoint: API endpoint to call
|
||||
headers: Request headers
|
||||
ctx: Optional context for logging (not used for resources)
|
||||
is_resource: Whether this request is for a resource
|
||||
|
||||
Returns:
|
||||
Parsed JSON response
|
||||
|
||||
Raises:
|
||||
GhostError: If there is an error accessing the Ghost API
|
||||
"""
|
||||
# Ensure clean URL construction with proper trailing slashes
|
||||
base_url = f"{API_URL.rstrip('/')}/ghost/api/admin"
|
||||
endpoint = endpoint.strip('/')
|
||||
url = f"{base_url}/{endpoint}/"
|
||||
|
||||
async with httpx.AsyncClient(follow_redirects=True) as client:
|
||||
try:
|
||||
response = await client.get(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
if not is_resource and ctx:
|
||||
ctx.log("info", f"API Request to {url} successful")
|
||||
return response.json()
|
||||
except httpx.HTTPError as e:
|
||||
error_msg = f"HTTP error accessing Ghost API: {str(e)}"
|
||||
if not is_resource and ctx:
|
||||
ctx.error(error_msg)
|
||||
raise GhostError(error_msg)
|
||||
except Exception as e:
|
||||
error_msg = f"Error accessing Ghost API: {str(e)}"
|
||||
if not is_resource and ctx:
|
||||
ctx.error(error_msg)
|
||||
raise GhostError(error_msg)
|
||||
18
src/ghost_mcp/config.py
Normal file
18
src/ghost_mcp/config.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Configuration settings for Ghost MCP server."""
|
||||
|
||||
import os
|
||||
from .exceptions import GhostError
|
||||
|
||||
# Ghost API Configuration
|
||||
API_URL = os.getenv("GHOST_API_URL")
|
||||
if not API_URL:
|
||||
raise GhostError("GHOST_API_URL environment variable is required")
|
||||
|
||||
STAFF_API_KEY = os.getenv("GHOST_STAFF_API_KEY")
|
||||
if not STAFF_API_KEY:
|
||||
raise GhostError("GHOST_STAFF_API_KEY environment variable is required")
|
||||
|
||||
# Server Configuration
|
||||
SERVER_NAME = "ghost"
|
||||
SERVER_DESCRIPTION = "Ghost blog integration providing access to posts, users, members, tiers, offers, newsletters and site information"
|
||||
SERVER_DEPENDENCIES = ["httpx", "pyjwt"]
|
||||
5
src/ghost_mcp/exceptions.py
Normal file
5
src/ghost_mcp/exceptions.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Custom exceptions for Ghost MCP server."""
|
||||
|
||||
class GhostError(Exception):
|
||||
"""Custom exception for Ghost API errors."""
|
||||
pass
|
||||
100
src/ghost_mcp/models.py
Normal file
100
src/ghost_mcp/models.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""Data models for Ghost MCP server."""
|
||||
|
||||
from typing import TypedDict, List, Optional
|
||||
|
||||
class Post(TypedDict):
|
||||
"""Ghost blog post data model."""
|
||||
id: str
|
||||
title: str
|
||||
status: str
|
||||
url: str
|
||||
created_at: str
|
||||
html: Optional[str]
|
||||
plaintext: Optional[str]
|
||||
excerpt: Optional[str]
|
||||
|
||||
class User(TypedDict):
|
||||
"""Ghost user data model."""
|
||||
id: str
|
||||
name: str
|
||||
email: str
|
||||
slug: str
|
||||
status: str
|
||||
location: Optional[str]
|
||||
website: Optional[str]
|
||||
bio: Optional[str]
|
||||
profile_image: Optional[str]
|
||||
cover_image: Optional[str]
|
||||
created_at: str
|
||||
last_seen: Optional[str]
|
||||
roles: List[dict]
|
||||
|
||||
class Member(TypedDict):
|
||||
"""Ghost member data model."""
|
||||
id: str
|
||||
name: str
|
||||
email: str
|
||||
status: str
|
||||
created_at: str
|
||||
note: Optional[str]
|
||||
labels: List[dict]
|
||||
email_count: int
|
||||
email_opened_count: int
|
||||
email_open_rate: float
|
||||
last_seen_at: Optional[str]
|
||||
newsletters: List[dict]
|
||||
subscriptions: List[dict]
|
||||
|
||||
class Tier(TypedDict):
|
||||
"""Ghost tier data model."""
|
||||
id: str
|
||||
name: str
|
||||
description: Optional[str]
|
||||
type: str
|
||||
active: bool
|
||||
welcome_page_url: Optional[str]
|
||||
created_at: str
|
||||
updated_at: str
|
||||
monthly_price: Optional[dict]
|
||||
yearly_price: Optional[dict]
|
||||
currency: str
|
||||
benefits: List[str]
|
||||
|
||||
class Offer(TypedDict):
|
||||
"""Ghost offer data model."""
|
||||
id: str
|
||||
name: str
|
||||
code: str
|
||||
display_title: str
|
||||
display_description: Optional[str]
|
||||
type: str
|
||||
status: str
|
||||
cadence: str
|
||||
amount: float
|
||||
duration: str
|
||||
currency: str
|
||||
tier: dict
|
||||
redemption_count: int
|
||||
created_at: str
|
||||
|
||||
class Newsletter(TypedDict):
|
||||
"""Ghost newsletter data model."""
|
||||
id: str
|
||||
name: str
|
||||
description: Optional[str]
|
||||
status: str
|
||||
visibility: str
|
||||
subscribe_on_signup: bool
|
||||
sort_order: int
|
||||
sender_name: str
|
||||
sender_email: Optional[str]
|
||||
sender_reply_to: Optional[str]
|
||||
show_header_icon: bool
|
||||
show_header_title: bool
|
||||
show_header_name: bool
|
||||
show_feature_image: bool
|
||||
title_font_category: str
|
||||
body_font_category: str
|
||||
show_badge: bool
|
||||
created_at: str
|
||||
updated_at: str
|
||||
162
src/ghost_mcp/resources.py
Normal file
162
src/ghost_mcp/resources.py
Normal file
@@ -0,0 +1,162 @@
|
||||
"""MCP resource handlers for Ghost API."""
|
||||
|
||||
import json
|
||||
import datetime
|
||||
from typing import Any
|
||||
|
||||
from .api import make_ghost_request, get_auth_headers
|
||||
from .config import API_URL, STAFF_API_KEY
|
||||
from .exceptions import GhostError
|
||||
|
||||
async def handle_user_resource(user_id: str) -> str:
|
||||
"""Get a user as a resource.
|
||||
|
||||
Args:
|
||||
user_id: The ID of the user to retrieve
|
||||
|
||||
Returns:
|
||||
JSON string containing user data
|
||||
"""
|
||||
try:
|
||||
headers = await get_auth_headers(STAFF_API_KEY)
|
||||
data = await make_ghost_request(
|
||||
f"users/{user_id}/?include=roles",
|
||||
headers,
|
||||
is_resource=True
|
||||
)
|
||||
user = data["users"][0]
|
||||
return json.dumps(user, indent=2)
|
||||
except GhostError as e:
|
||||
return json.dumps({"error": str(e)}, indent=2)
|
||||
|
||||
async def handle_member_resource(member_id: str) -> str:
|
||||
"""Get a member as a resource.
|
||||
|
||||
Args:
|
||||
member_id: The ID of the member to retrieve
|
||||
|
||||
Returns:
|
||||
JSON string containing member data
|
||||
"""
|
||||
try:
|
||||
headers = await get_auth_headers(STAFF_API_KEY)
|
||||
data = await make_ghost_request(
|
||||
f"members/{member_id}/?include=newsletters,subscriptions",
|
||||
headers,
|
||||
is_resource=True
|
||||
)
|
||||
member = data["members"][0]
|
||||
return json.dumps(member, indent=2)
|
||||
except GhostError as e:
|
||||
return json.dumps({"error": str(e)}, indent=2)
|
||||
|
||||
async def handle_tier_resource(tier_id: str) -> str:
|
||||
"""Get a tier as a resource.
|
||||
|
||||
Args:
|
||||
tier_id: The ID of the tier to retrieve
|
||||
|
||||
Returns:
|
||||
JSON string containing tier data
|
||||
"""
|
||||
try:
|
||||
headers = await get_auth_headers(STAFF_API_KEY)
|
||||
data = await make_ghost_request(
|
||||
f"tiers/{tier_id}/?include=monthly_price,yearly_price,benefits",
|
||||
headers,
|
||||
is_resource=True
|
||||
)
|
||||
tier = data["tiers"][0]
|
||||
return json.dumps(tier, indent=2)
|
||||
except GhostError as e:
|
||||
return json.dumps({"error": str(e)}, indent=2)
|
||||
|
||||
async def handle_offer_resource(offer_id: str) -> str:
|
||||
"""Get an offer as a resource.
|
||||
|
||||
Args:
|
||||
offer_id: The ID of the offer to retrieve
|
||||
|
||||
Returns:
|
||||
JSON string containing offer data
|
||||
"""
|
||||
try:
|
||||
headers = await get_auth_headers(STAFF_API_KEY)
|
||||
data = await make_ghost_request(
|
||||
f"offers/{offer_id}/",
|
||||
headers,
|
||||
is_resource=True
|
||||
)
|
||||
offer = data["offers"][0]
|
||||
return json.dumps(offer, indent=2)
|
||||
except GhostError as e:
|
||||
return json.dumps({"error": str(e)}, indent=2)
|
||||
|
||||
async def handle_newsletter_resource(newsletter_id: str) -> str:
|
||||
"""Get a newsletter as a resource.
|
||||
|
||||
Args:
|
||||
newsletter_id: The ID of the newsletter to retrieve
|
||||
|
||||
Returns:
|
||||
JSON string containing newsletter data
|
||||
"""
|
||||
try:
|
||||
headers = await get_auth_headers(STAFF_API_KEY)
|
||||
data = await make_ghost_request(
|
||||
f"newsletters/{newsletter_id}/",
|
||||
headers,
|
||||
is_resource=True
|
||||
)
|
||||
newsletter = data["newsletters"][0]
|
||||
return json.dumps(newsletter, indent=2)
|
||||
except GhostError as e:
|
||||
return json.dumps({"error": str(e)}, indent=2)
|
||||
|
||||
async def handle_post_resource(post_id: str) -> str:
|
||||
"""Get a blog post as a resource.
|
||||
|
||||
Args:
|
||||
post_id: The ID of the post to retrieve
|
||||
|
||||
Returns:
|
||||
JSON string containing post data
|
||||
"""
|
||||
try:
|
||||
headers = await get_auth_headers(STAFF_API_KEY)
|
||||
data = await make_ghost_request(
|
||||
f"posts/{post_id}/?formats=html",
|
||||
headers,
|
||||
is_resource=True
|
||||
)
|
||||
post = data["posts"][0]
|
||||
|
||||
return json.dumps({
|
||||
"title": post.get('title'),
|
||||
"html": post.get('html'),
|
||||
"excerpt": post.get('excerpt'),
|
||||
"url": post.get('url'),
|
||||
"created_at": post.get('created_at')
|
||||
}, indent=2)
|
||||
except GhostError as e:
|
||||
return json.dumps({"error": str(e)}, indent=2)
|
||||
|
||||
async def handle_blog_info() -> str:
|
||||
"""Get general blog information as a resource.
|
||||
|
||||
Returns:
|
||||
JSON string containing blog information
|
||||
"""
|
||||
try:
|
||||
headers = await get_auth_headers(STAFF_API_KEY)
|
||||
data = await make_ghost_request("site", headers, is_resource=True)
|
||||
site = data["site"]
|
||||
|
||||
return json.dumps({
|
||||
"title": site.get('title'),
|
||||
"description": site.get('description'),
|
||||
"url": API_URL,
|
||||
"last_updated": datetime.datetime.now(datetime.UTC).isoformat()
|
||||
}, indent=2)
|
||||
except GhostError as e:
|
||||
return json.dumps({"error": str(e)}, indent=2)
|
||||
83
src/ghost_mcp/server.py
Normal file
83
src/ghost_mcp/server.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""MCP server setup and initialization."""
|
||||
|
||||
from mcp.server.fastmcp import FastMCP, Context
|
||||
from . import tools, resources
|
||||
from .config import (
|
||||
SERVER_NAME,
|
||||
SERVER_DEPENDENCIES,
|
||||
SERVER_DESCRIPTION
|
||||
)
|
||||
from .exceptions import GhostError
|
||||
|
||||
def create_server() -> FastMCP:
|
||||
"""Create and configure the Ghost MCP server.
|
||||
|
||||
Returns:
|
||||
Configured FastMCP server instance
|
||||
"""
|
||||
# Initialize FastMCP server
|
||||
mcp = FastMCP(
|
||||
SERVER_NAME,
|
||||
dependencies=SERVER_DEPENDENCIES,
|
||||
description=SERVER_DESCRIPTION,
|
||||
log_level="WARNING" # Set log level to reduce verbosity
|
||||
)
|
||||
|
||||
# Set up error handler
|
||||
async def handle_error(error: Exception) -> None:
|
||||
if isinstance(error, GhostError):
|
||||
mcp.log.error(f"Ghost API Error: {str(error)}")
|
||||
else:
|
||||
mcp.log.error(f"Server Error: {str(error)}")
|
||||
|
||||
mcp.on_error = handle_error
|
||||
|
||||
# Register resource handlers
|
||||
mcp.resource("user://{user_id}")(resources.handle_user_resource)
|
||||
mcp.resource("member://{member_id}")(resources.handle_member_resource)
|
||||
mcp.resource("tier://{tier_id}")(resources.handle_tier_resource)
|
||||
mcp.resource("offer://{offer_id}")(resources.handle_offer_resource)
|
||||
mcp.resource("newsletter://{newsletter_id}")(resources.handle_newsletter_resource)
|
||||
mcp.resource("post://{post_id}")(resources.handle_post_resource)
|
||||
mcp.resource("blog://info")(resources.handle_blog_info)
|
||||
|
||||
# Register tools
|
||||
mcp.tool()(tools.list_users)
|
||||
mcp.tool()(tools.list_members)
|
||||
mcp.tool()(tools.list_tiers)
|
||||
mcp.tool()(tools.list_offers)
|
||||
mcp.tool()(tools.list_newsletters)
|
||||
mcp.tool()(tools.list_posts)
|
||||
mcp.tool()(tools.read_post)
|
||||
mcp.tool()(tools.search_posts_by_title)
|
||||
mcp.tool()(tools.read_user)
|
||||
mcp.tool()(tools.read_member)
|
||||
mcp.tool()(tools.read_tier)
|
||||
mcp.tool()(tools.read_offer)
|
||||
mcp.tool()(tools.read_newsletter)
|
||||
|
||||
# Register prompts
|
||||
@mcp.prompt()
|
||||
def search_blog() -> str:
|
||||
"""Prompt template for searching blog posts"""
|
||||
return """I want to help you search the blog posts. You can:
|
||||
1. Search by title with: search_posts_by_title("your search term")
|
||||
2. List all posts with: list_posts()
|
||||
3. Read a specific post with: read_post("post_id")
|
||||
|
||||
What would you like to search for?"""
|
||||
|
||||
@mcp.prompt()
|
||||
def create_summary(post_id: str) -> str:
|
||||
"""Create a prompt to summarize a blog post"""
|
||||
return f"""Please read the following blog post and provide a concise summary:
|
||||
|
||||
Resource: post://{post_id}
|
||||
|
||||
Key points to include:
|
||||
1. Main topic/theme
|
||||
2. Key arguments or insights
|
||||
3. Important conclusions
|
||||
4. Any actionable takeaways"""
|
||||
|
||||
return mcp
|
||||
603
src/ghost_mcp/tools.py
Normal file
603
src/ghost_mcp/tools.py
Normal file
@@ -0,0 +1,603 @@
|
||||
"""MCP tool implementations for Ghost API."""
|
||||
|
||||
import json
|
||||
from difflib import get_close_matches
|
||||
from mcp.server.fastmcp import FastMCP, Context
|
||||
|
||||
from .api import make_ghost_request, get_auth_headers
|
||||
from .config import STAFF_API_KEY
|
||||
from .exceptions import GhostError
|
||||
|
||||
async def read_user(user_id: str, ctx: Context = None) -> str:
|
||||
"""Get the details of a specific user.
|
||||
|
||||
Args:
|
||||
user_id: The ID of the user to retrieve
|
||||
ctx: Optional context for logging
|
||||
|
||||
Returns:
|
||||
Formatted string containing the user details
|
||||
"""
|
||||
try:
|
||||
headers = await get_auth_headers(STAFF_API_KEY)
|
||||
data = await make_ghost_request(
|
||||
f"users/{user_id}/?include=roles",
|
||||
headers,
|
||||
ctx
|
||||
)
|
||||
|
||||
user = data["users"][0]
|
||||
roles = [role.get('name') for role in user.get('roles', [])]
|
||||
|
||||
return f"""
|
||||
Name: {user.get('name', 'Unknown')}
|
||||
Email: {user.get('email', 'Unknown')}
|
||||
Slug: {user.get('slug', 'Unknown')}
|
||||
Status: {user.get('status', 'Unknown')}
|
||||
Roles: {', '.join(roles)}
|
||||
Location: {user.get('location', 'Not specified')}
|
||||
Website: {user.get('website', 'None')}
|
||||
Bio: {user.get('bio', 'No bio')}
|
||||
Profile Image: {user.get('profile_image', 'None')}
|
||||
Cover Image: {user.get('cover_image', 'None')}
|
||||
Created: {user.get('created_at', 'Unknown')}
|
||||
Last Seen: {user.get('last_seen', 'Never')}
|
||||
"""
|
||||
except GhostError as e:
|
||||
return str(e)
|
||||
|
||||
async def read_member(member_id: str, ctx: Context = None) -> str:
|
||||
"""Get the details of a specific member.
|
||||
|
||||
Args:
|
||||
member_id: The ID of the member to retrieve
|
||||
ctx: Optional context for logging
|
||||
|
||||
Returns:
|
||||
Formatted string containing the member details
|
||||
"""
|
||||
try:
|
||||
headers = await get_auth_headers(STAFF_API_KEY)
|
||||
data = await make_ghost_request(
|
||||
f"members/{member_id}/?include=newsletters,subscriptions",
|
||||
headers,
|
||||
ctx
|
||||
)
|
||||
|
||||
member = data["members"][0]
|
||||
newsletters = [nl.get('name') for nl in member.get('newsletters', [])]
|
||||
subscriptions = member.get('subscriptions', [])
|
||||
|
||||
subscription_info = ""
|
||||
if subscriptions:
|
||||
for sub in subscriptions:
|
||||
subscription_info += f"""
|
||||
Subscription Details:
|
||||
Status: {sub.get('status', 'Unknown')}
|
||||
Start Date: {sub.get('start_date', 'Unknown')}
|
||||
Current Period Ends: {sub.get('current_period_end', 'Unknown')}
|
||||
Price: {sub.get('price', {}).get('nickname', 'Unknown')} ({sub.get('price', {}).get('amount', 0)} {sub.get('price', {}).get('currency', 'USD')})
|
||||
"""
|
||||
|
||||
return f"""
|
||||
Name: {member.get('name', 'Unknown')}
|
||||
Email: {member.get('email', 'Unknown')}
|
||||
Status: {member.get('status', 'Unknown')}
|
||||
Newsletters: {', '.join(newsletters) if newsletters else 'None'}
|
||||
Created: {member.get('created_at', 'Unknown')}
|
||||
Note: {member.get('note', 'No notes')}
|
||||
Labels: {', '.join(label.get('name', '') for label in member.get('labels', []))}
|
||||
Email Count: {member.get('email_count', 0)}
|
||||
Email Opened Count: {member.get('email_opened_count', 0)}
|
||||
Email Open Rate: {member.get('email_open_rate', 0)}%
|
||||
Last Seen At: {member.get('last_seen_at', 'Never')}{subscription_info}
|
||||
"""
|
||||
except GhostError as e:
|
||||
return str(e)
|
||||
|
||||
async def read_tier(tier_id: str, ctx: Context = None) -> str:
|
||||
"""Get the details of a specific tier.
|
||||
|
||||
Args:
|
||||
tier_id: The ID of the tier to retrieve
|
||||
ctx: Optional context for logging
|
||||
|
||||
Returns:
|
||||
Formatted string containing the tier details
|
||||
"""
|
||||
try:
|
||||
headers = await get_auth_headers(STAFF_API_KEY)
|
||||
data = await make_ghost_request(
|
||||
f"tiers/{tier_id}/?include=monthly_price,yearly_price,benefits",
|
||||
headers,
|
||||
ctx
|
||||
)
|
||||
|
||||
tier = data["tiers"][0]
|
||||
benefits = tier.get('benefits', [])
|
||||
|
||||
return f"""
|
||||
Name: {tier.get('name', 'Unknown')}
|
||||
Description: {tier.get('description', 'No description')}
|
||||
Type: {tier.get('type', 'Unknown')}
|
||||
Active: {tier.get('active', False)}
|
||||
Welcome Page URL: {tier.get('welcome_page_url', 'None')}
|
||||
Created: {tier.get('created_at', 'Unknown')}
|
||||
Updated: {tier.get('updated_at', 'Unknown')}
|
||||
Monthly Price: {tier.get('monthly_price', 'N/A')}
|
||||
Yearly Price: {tier.get('yearly_price', 'N/A')}
|
||||
Currency: {tier.get('currency', 'Unknown')}
|
||||
Benefits:
|
||||
{chr(10).join(f'- {benefit}' for benefit in benefits) if benefits else 'No benefits listed'}
|
||||
"""
|
||||
except GhostError as e:
|
||||
return str(e)
|
||||
|
||||
async def read_offer(offer_id: str, ctx: Context = None) -> str:
|
||||
"""Get the details of a specific offer.
|
||||
|
||||
Args:
|
||||
offer_id: The ID of the offer to retrieve
|
||||
ctx: Optional context for logging
|
||||
|
||||
Returns:
|
||||
Formatted string containing the offer details
|
||||
"""
|
||||
try:
|
||||
headers = await get_auth_headers(STAFF_API_KEY)
|
||||
data = await make_ghost_request(
|
||||
f"offers/{offer_id}/",
|
||||
headers,
|
||||
ctx
|
||||
)
|
||||
|
||||
offer = data["offers"][0]
|
||||
|
||||
return f"""
|
||||
Name: {offer.get('name', 'Unknown')}
|
||||
Code: {offer.get('code', 'Unknown')}
|
||||
Display Title: {offer.get('display_title', 'No display title')}
|
||||
Display Description: {offer.get('display_description', 'No description')}
|
||||
Type: {offer.get('type', 'Unknown')}
|
||||
Status: {offer.get('status', 'Unknown')}
|
||||
Cadence: {offer.get('cadence', 'Unknown')}
|
||||
Amount: {offer.get('amount', 'Unknown')}
|
||||
Duration: {offer.get('duration', 'Unknown')}
|
||||
Currency: {offer.get('currency', 'N/A')}
|
||||
Tier: {offer.get('tier', {}).get('name', 'Unknown')}
|
||||
Redemption Count: {offer.get('redemption_count', 0)}
|
||||
Created: {offer.get('created_at', 'Unknown')}
|
||||
"""
|
||||
except GhostError as e:
|
||||
return str(e)
|
||||
|
||||
async def read_newsletter(newsletter_id: str, ctx: Context = None) -> str:
|
||||
"""Get the details of a specific newsletter.
|
||||
|
||||
Args:
|
||||
newsletter_id: The ID of the newsletter to retrieve
|
||||
ctx: Optional context for logging
|
||||
|
||||
Returns:
|
||||
Formatted string containing the newsletter details
|
||||
"""
|
||||
try:
|
||||
headers = await get_auth_headers(STAFF_API_KEY)
|
||||
data = await make_ghost_request(
|
||||
f"newsletters/{newsletter_id}/",
|
||||
headers,
|
||||
ctx
|
||||
)
|
||||
|
||||
newsletter = data["newsletters"][0]
|
||||
|
||||
return f"""
|
||||
Name: {newsletter.get('name', 'Unknown')}
|
||||
Description: {newsletter.get('description', 'No description')}
|
||||
Status: {newsletter.get('status', 'Unknown')}
|
||||
Visibility: {newsletter.get('visibility', 'Unknown')}
|
||||
Subscribe on Signup: {newsletter.get('subscribe_on_signup', False)}
|
||||
Sort Order: {newsletter.get('sort_order', 0)}
|
||||
Sender Name: {newsletter.get('sender_name', 'Unknown')}
|
||||
Sender Email: {newsletter.get('sender_email', 'Not set')}
|
||||
Sender Reply To: {newsletter.get('sender_reply_to', 'Not set')}
|
||||
Show Header Icon: {newsletter.get('show_header_icon', True)}
|
||||
Show Header Title: {newsletter.get('show_header_title', True)}
|
||||
Show Header Name: {newsletter.get('show_header_name', True)}
|
||||
Show Feature Image: {newsletter.get('show_feature_image', True)}
|
||||
Title Font Category: {newsletter.get('title_font_category', 'Unknown')}
|
||||
Body Font Category: {newsletter.get('body_font_category', 'Unknown')}
|
||||
Show Badge: {newsletter.get('show_badge', True)}
|
||||
Created: {newsletter.get('created_at', 'Unknown')}
|
||||
Updated: {newsletter.get('updated_at', 'Unknown')}
|
||||
"""
|
||||
except GhostError as e:
|
||||
return str(e)
|
||||
|
||||
async def list_users(
|
||||
format: str = "text",
|
||||
page: int = 1,
|
||||
limit: int = 15,
|
||||
ctx: Context = None
|
||||
) -> str:
|
||||
"""Get the list of users from your Ghost blog.
|
||||
|
||||
Args:
|
||||
format: Output format - either "text" or "json" (default: "text")
|
||||
page: Page number for pagination (default: 1)
|
||||
limit: Number of users per page (default: 15)
|
||||
ctx: Optional context for logging
|
||||
|
||||
Returns:
|
||||
Formatted string containing user information
|
||||
"""
|
||||
try:
|
||||
headers = await get_auth_headers(STAFF_API_KEY)
|
||||
data = await make_ghost_request(
|
||||
f"users/?page={page}&limit={limit}&include=roles",
|
||||
headers,
|
||||
ctx
|
||||
)
|
||||
|
||||
users = data.get("users", [])
|
||||
if not users:
|
||||
return "No users found."
|
||||
|
||||
if format.lower() == "json":
|
||||
return json.dumps(users, indent=2)
|
||||
|
||||
formatted_users = []
|
||||
for user in users:
|
||||
roles = [role.get('name') for role in user.get('roles', [])]
|
||||
formatted_user = f"""
|
||||
Name: {user.get('name', 'Unknown')}
|
||||
Email: {user.get('email', 'Unknown')}
|
||||
Roles: {', '.join(roles)}
|
||||
Status: {user.get('status', 'Unknown')}
|
||||
ID: {user.get('id', 'Unknown')}
|
||||
"""
|
||||
formatted_users.append(formatted_user)
|
||||
return "\n---\n".join(formatted_users)
|
||||
|
||||
except GhostError as e:
|
||||
return str(e)
|
||||
|
||||
async def list_members(
|
||||
format: str = "text",
|
||||
page: int = 1,
|
||||
limit: int = 15,
|
||||
ctx: Context = None
|
||||
) -> str:
|
||||
"""Get the list of members from your Ghost blog.
|
||||
|
||||
Args:
|
||||
format: Output format - either "text" or "json" (default: "text")
|
||||
page: Page number for pagination (default: 1)
|
||||
limit: Number of members per page (default: 15)
|
||||
ctx: Optional context for logging
|
||||
|
||||
Returns:
|
||||
Formatted string containing member information
|
||||
"""
|
||||
try:
|
||||
headers = await get_auth_headers(STAFF_API_KEY)
|
||||
data = await make_ghost_request(
|
||||
f"members/?page={page}&limit={limit}&include=newsletters,subscriptions",
|
||||
headers,
|
||||
ctx
|
||||
)
|
||||
|
||||
members = data.get("members", [])
|
||||
if not members:
|
||||
return "No members found."
|
||||
|
||||
if format.lower() == "json":
|
||||
return json.dumps(members, indent=2)
|
||||
|
||||
formatted_members = []
|
||||
for member in members:
|
||||
newsletters = [nl.get('name') for nl in member.get('newsletters', [])]
|
||||
formatted_member = f"""
|
||||
Name: {member.get('name', 'Unknown')}
|
||||
Email: {member.get('email', 'Unknown')}
|
||||
Status: {member.get('status', 'Unknown')}
|
||||
Newsletters: {', '.join(newsletters) if newsletters else 'None'}
|
||||
Created: {member.get('created_at', 'Unknown')}
|
||||
ID: {member.get('id', 'Unknown')}
|
||||
"""
|
||||
formatted_members.append(formatted_member)
|
||||
return "\n---\n".join(formatted_members)
|
||||
|
||||
except GhostError as e:
|
||||
return str(e)
|
||||
|
||||
async def list_tiers(
|
||||
format: str = "text",
|
||||
page: int = 1,
|
||||
limit: int = 15,
|
||||
ctx: Context = None
|
||||
) -> str:
|
||||
"""Get the list of tiers from your Ghost blog.
|
||||
|
||||
Args:
|
||||
format: Output format - either "text" or "json" (default: "text")
|
||||
page: Page number for pagination (default: 1)
|
||||
limit: Number of tiers per page (default: 15)
|
||||
ctx: Optional context for logging
|
||||
|
||||
Returns:
|
||||
Formatted string containing tier information
|
||||
"""
|
||||
try:
|
||||
headers = await get_auth_headers(STAFF_API_KEY)
|
||||
data = await make_ghost_request(
|
||||
f"tiers/?page={page}&limit={limit}&include=monthly_price,yearly_price,benefits",
|
||||
headers,
|
||||
ctx
|
||||
)
|
||||
|
||||
tiers = data.get("tiers", [])
|
||||
if not tiers:
|
||||
return "No tiers found."
|
||||
|
||||
if format.lower() == "json":
|
||||
return json.dumps(tiers, indent=2)
|
||||
|
||||
formatted_tiers = []
|
||||
for tier in tiers:
|
||||
benefits = tier.get('benefits', [])
|
||||
formatted_tier = f"""
|
||||
Name: {tier.get('name', 'Unknown')}
|
||||
Description: {tier.get('description', 'No description')}
|
||||
Type: {tier.get('type', 'Unknown')}
|
||||
Active: {tier.get('active', False)}
|
||||
Monthly Price: {tier.get('monthly_price', 'N/A')}
|
||||
Yearly Price: {tier.get('yearly_price', 'N/A')}
|
||||
Benefits: {', '.join(benefits) if benefits else 'None'}
|
||||
ID: {tier.get('id', 'Unknown')}
|
||||
"""
|
||||
formatted_tiers.append(formatted_tier)
|
||||
return "\n---\n".join(formatted_tiers)
|
||||
|
||||
except GhostError as e:
|
||||
return str(e)
|
||||
|
||||
async def list_offers(
|
||||
format: str = "text",
|
||||
page: int = 1,
|
||||
limit: int = 15,
|
||||
ctx: Context = None
|
||||
) -> str:
|
||||
"""Get the list of offers from your Ghost blog.
|
||||
|
||||
Args:
|
||||
format: Output format - either "text" or "json" (default: "text")
|
||||
page: Page number for pagination (default: 1)
|
||||
limit: Number of offers per page (default: 15)
|
||||
ctx: Optional context for logging
|
||||
|
||||
Returns:
|
||||
Formatted string containing offer information
|
||||
"""
|
||||
try:
|
||||
headers = await get_auth_headers(STAFF_API_KEY)
|
||||
data = await make_ghost_request(
|
||||
f"offers/?page={page}&limit={limit}",
|
||||
headers,
|
||||
ctx
|
||||
)
|
||||
|
||||
offers = data.get("offers", [])
|
||||
if not offers:
|
||||
return "No offers found."
|
||||
|
||||
if format.lower() == "json":
|
||||
return json.dumps(offers, indent=2)
|
||||
|
||||
formatted_offers = []
|
||||
for offer in offers:
|
||||
formatted_offer = f"""
|
||||
Name: {offer.get('name', 'Unknown')}
|
||||
Code: {offer.get('code', 'Unknown')}
|
||||
Display Title: {offer.get('display_title', 'No display title')}
|
||||
Type: {offer.get('type', 'Unknown')}
|
||||
Amount: {offer.get('amount', 'Unknown')}
|
||||
Duration: {offer.get('duration', 'Unknown')}
|
||||
Status: {offer.get('status', 'Unknown')}
|
||||
Tier: {offer.get('tier', {}).get('name', 'Unknown')}
|
||||
ID: {offer.get('id', 'Unknown')}
|
||||
"""
|
||||
formatted_offers.append(formatted_offer)
|
||||
return "\n---\n".join(formatted_offers)
|
||||
|
||||
except GhostError as e:
|
||||
return str(e)
|
||||
|
||||
async def list_newsletters(
|
||||
format: str = "text",
|
||||
page: int = 1,
|
||||
limit: int = 15,
|
||||
ctx: Context = None
|
||||
) -> str:
|
||||
"""Get the list of newsletters from your Ghost blog.
|
||||
|
||||
Args:
|
||||
format: Output format - either "text" or "json" (default: "text")
|
||||
page: Page number for pagination (default: 1)
|
||||
limit: Number of newsletters per page (default: 15)
|
||||
ctx: Optional context for logging
|
||||
|
||||
Returns:
|
||||
Formatted string containing newsletter information
|
||||
"""
|
||||
try:
|
||||
headers = await get_auth_headers(STAFF_API_KEY)
|
||||
data = await make_ghost_request(
|
||||
f"newsletters/?page={page}&limit={limit}",
|
||||
headers,
|
||||
ctx
|
||||
)
|
||||
|
||||
newsletters = data.get("newsletters", [])
|
||||
if not newsletters:
|
||||
return "No newsletters found."
|
||||
|
||||
if format.lower() == "json":
|
||||
return json.dumps(newsletters, indent=2)
|
||||
|
||||
formatted_newsletters = []
|
||||
for newsletter in newsletters:
|
||||
formatted_newsletter = f"""
|
||||
Name: {newsletter.get('name', 'Unknown')}
|
||||
Description: {newsletter.get('description', 'No description')}
|
||||
Status: {newsletter.get('status', 'Unknown')}
|
||||
Visibility: {newsletter.get('visibility', 'Unknown')}
|
||||
Subscribe on Signup: {newsletter.get('subscribe_on_signup', False)}
|
||||
Sender Name: {newsletter.get('sender_name', 'Unknown')}
|
||||
ID: {newsletter.get('id', 'Unknown')}
|
||||
"""
|
||||
formatted_newsletters.append(formatted_newsletter)
|
||||
return "\n---\n".join(formatted_newsletters)
|
||||
|
||||
except GhostError as e:
|
||||
return str(e)
|
||||
|
||||
async def list_posts(
|
||||
format: str = "text",
|
||||
page: int = 1,
|
||||
limit: int = 15,
|
||||
ctx: Context = None
|
||||
) -> str:
|
||||
"""Get the list of posts from your Ghost blog.
|
||||
|
||||
Args:
|
||||
format: Output format - either "text" or "json" (default: "text")
|
||||
page: Page number for pagination (default: 1)
|
||||
limit: Number of posts per page (default: 15)
|
||||
ctx: Optional context for logging
|
||||
|
||||
Returns:
|
||||
Formatted string containing post information
|
||||
|
||||
Raises:
|
||||
GhostError: If there is an error accessing the Ghost API
|
||||
"""
|
||||
try:
|
||||
headers = await get_auth_headers(STAFF_API_KEY)
|
||||
data = await make_ghost_request(
|
||||
f"posts/?page={page}&limit={limit}",
|
||||
headers,
|
||||
ctx
|
||||
)
|
||||
|
||||
posts = data.get("posts", [])
|
||||
if not posts:
|
||||
return "No posts found."
|
||||
|
||||
if format.lower() == "json":
|
||||
formatted_posts = [{
|
||||
"id": post.get('id', 'Unknown'),
|
||||
"title": post.get('title', 'Untitled'),
|
||||
"status": post.get('status', 'Unknown'),
|
||||
"url": post.get('url', 'No URL'),
|
||||
"created_at": post.get('created_at', 'Unknown')
|
||||
} for post in posts]
|
||||
return json.dumps(formatted_posts, indent=2)
|
||||
|
||||
formatted_posts = []
|
||||
for post in posts:
|
||||
formatted_post = f"""
|
||||
Title: {post.get('title', 'Untitled')}
|
||||
Status: {post.get('status', 'Unknown')}
|
||||
URL: {post.get('url', 'No URL')}
|
||||
Created: {post.get('created_at', 'Unknown')}
|
||||
ID: {post.get('id', 'Unknown')}
|
||||
"""
|
||||
formatted_posts.append(formatted_post)
|
||||
return "\n---\n".join(formatted_posts)
|
||||
|
||||
except GhostError as e:
|
||||
return str(e)
|
||||
|
||||
async def read_post(post_id: str, ctx: Context = None) -> str:
|
||||
"""Get the full content of a specific blog post.
|
||||
|
||||
Args:
|
||||
post_id: The ID of the post to retrieve
|
||||
ctx: Optional context for logging
|
||||
|
||||
Returns:
|
||||
Formatted string containing the full post content
|
||||
|
||||
Raises:
|
||||
GhostError: If there is an error accessing the Ghost API
|
||||
"""
|
||||
try:
|
||||
headers = await get_auth_headers(STAFF_API_KEY)
|
||||
data = await make_ghost_request(
|
||||
f"posts/{post_id}/?formats=html,plaintext&include=html,plaintext",
|
||||
headers,
|
||||
ctx
|
||||
)
|
||||
|
||||
post = data["posts"][0]
|
||||
content = post.get('html') or post.get('plaintext') or 'No content available'
|
||||
|
||||
return f"""
|
||||
Title: {post.get('title', 'Untitled')}
|
||||
Status: {post.get('status', 'Unknown')}
|
||||
URL: {post.get('url', 'No URL')}
|
||||
Created: {post.get('created_at', 'Unknown')}
|
||||
|
||||
Content:
|
||||
{content}
|
||||
"""
|
||||
except GhostError as e:
|
||||
return str(e)
|
||||
|
||||
async def search_posts_by_title(query: str, exact: bool = False, ctx: Context = None) -> str:
|
||||
"""Search for posts by title.
|
||||
|
||||
Args:
|
||||
query: The title or part of the title to search for
|
||||
exact: If True, only return exact matches (default: False)
|
||||
ctx: Optional context for logging
|
||||
|
||||
Returns:
|
||||
Formatted string containing matching post information
|
||||
|
||||
Raises:
|
||||
GhostError: If there is an error accessing the Ghost API
|
||||
"""
|
||||
try:
|
||||
headers = await get_auth_headers(STAFF_API_KEY)
|
||||
data = await make_ghost_request("posts", headers, ctx)
|
||||
|
||||
posts = data.get("posts", [])
|
||||
matches = []
|
||||
|
||||
if exact:
|
||||
matches = [post for post in posts if post.get('title', '').lower() == query.lower()]
|
||||
else:
|
||||
titles = [post.get('title', '') for post in posts]
|
||||
matching_titles = get_close_matches(query, titles, n=5, cutoff=0.3)
|
||||
matches = [post for post in posts if post.get('title', '') in matching_titles]
|
||||
|
||||
if not matches:
|
||||
return f"No posts found matching '{query}'"
|
||||
|
||||
formatted_matches = []
|
||||
for post in matches:
|
||||
formatted_match = f"""
|
||||
Title: {post.get('title', 'Untitled')}
|
||||
Status: {post.get('status', 'Unknown')}
|
||||
URL: {post.get('url', 'No URL')}
|
||||
Created: {post.get('created_at', 'Unknown')}
|
||||
ID: {post.get('id', 'Unknown')}
|
||||
"""
|
||||
formatted_matches.append(formatted_match)
|
||||
|
||||
return "\n---\n".join(formatted_matches)
|
||||
|
||||
except GhostError as e:
|
||||
return str(e)
|
||||
12
src/main.py
Normal file
12
src/main.py
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Main entry point for Ghost MCP server."""
|
||||
|
||||
from ghost_mcp import create_server
|
||||
|
||||
def main():
|
||||
"""Initialize and run the Ghost MCP server."""
|
||||
mcp = create_server()
|
||||
mcp.run(transport='stdio')
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
359
uv.lock
generated
Normal file
359
uv.lock
generated
Normal file
@@ -0,0 +1,359 @@
|
||||
version = 1
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.1.31"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghost-mcp"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
{ name = "mcp", extra = ["cli"] },
|
||||
{ name = "pyjwt" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "httpx" },
|
||||
{ name = "mcp", extras = ["cli"], specifier = ">=1.2.1" },
|
||||
{ name = "pyjwt" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.14.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx-sse"
|
||||
version = "0.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "3.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcp"
|
||||
version = "1.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "httpx" },
|
||||
{ name = "httpx-sse" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "sse-starlette" },
|
||||
{ name = "starlette" },
|
||||
{ name = "uvicorn" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/30/51e4555826126e3954fa2ab1e934bf74163c5fe05e98f38ca4d0f8abbf63/mcp-1.2.1.tar.gz", hash = "sha256:c9d43dbfe943aa1530e2be8f54b73af3ebfb071243827b4483d421684806cb45", size = 103968 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/0d/6770742a84c8aa1d36c0d628896a380584c5759612e66af7446af07d8775/mcp-1.2.1-py3-none-any.whl", hash = "sha256:579bf9c9157850ebb1344f3ca6f7a3021b0123c44c9f089ef577a7062522f0fd", size = 66453 },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
cli = [
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "typer" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.10.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.27.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/73/7b/c58a586cd7d9ac66d2ee4ba60ca2d241fa837c02bca9bea80a9a8c3d22a9/pydantic_settings-2.7.1.tar.gz", hash = "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93", size = 79920 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/46/93416fdae86d40879714f72956ac14df9c7b76f7d41a4d68aa9f71a0028b/pydantic_settings-2.7.1-py3-none-any.whl", hash = "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd", size = 29718 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyjwt"
|
||||
version = "2.10.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "13.9.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellingham"
|
||||
version = "1.5.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sse-starlette"
|
||||
version = "2.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "starlette" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.45.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ff/fb/2984a686808b89a6781526129a4b51266f678b2d2b97ab2d325e56116df8/starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f", size = 2574076 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/61/f2b52e107b1fc8944b33ef56bf6ac4ebbe16d91b94d2b87ce013bf63fb84/starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d", size = 71507 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.15.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "rich" },
|
||||
{ name = "shellingham" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/dca7b219718afd37a0068f4f2530a727c2b74a8b6e8e0c0080a4c0de4fcd/typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a", size = 99789 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/cc/0a838ba5ca64dc832aa43f727bd586309846b0ffb2ce52422543e6075e8a/typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847", size = 44908 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.34.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 },
|
||||
]
|
||||
Reference in New Issue
Block a user