fetch
Retrieve specific blog posts from Contraption Company by providing an identifier such as a slug, URL, or post ID.
Instructions
Fetch a blog post using the MCP HTTP-style contract.
Accepts an id that can be a slug, canonical URL, or a post:// style identifier.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| id | Yes |
Output Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- src/mcp_server.py:82-153 (handler)The handler function for the 'fetch' tool. It validates the input id, extracts the post slug, retrieves the post data and markdown from ChromaService, resolves a canonical URL, and returns an HTTP-like response dictionary with the post details.
@mcp.tool(name="fetch") async def fetch(id: str) -> dict[str, Any]: """Fetch a blog post using the MCP HTTP-style contract. Accepts an ``id`` that can be a slug, canonical URL, or a ``post://`` style identifier. """ if not id: return { "status": {"code": 400, "text": "Bad Request"}, "headers": {"Content-Type": "application/json"}, "body": { "kind": "text", "text": json.dumps({"error": "An 'id' must be provided"}), }, } slug = _extract_slug_from_url(id) if not slug: return { "status": {"code": 400, "text": "Bad Request"}, "headers": {"Content-Type": "application/json"}, "body": { "kind": "text", "text": json.dumps({"error": "Unable to determine post slug from identifier"}), }, } chroma_service = await get_chroma_service() post_summary, markdown = await chroma_service.get_post_markdown(slug) if not post_summary or markdown is None: return { "status": {"code": 404, "text": "Not Found"}, "headers": {"Content-Type": "application/json"}, "body": { "kind": "text", "text": json.dumps({"error": "Post not found"}), }, } resolved_url = _canonical_post_url(post_summary, id) if not resolved_url: logger.warning("Unable to resolve canonical URL for post %s", post_summary.id) return { "status": {"code": 502, "text": "Bad Gateway"}, "headers": {"Content-Type": "application/json"}, "body": { "kind": "text", "text": json.dumps({"error": "Unable to resolve canonical URL for post"}), }, } response_body = { "id": resolved_url, "title": post_summary.title, "excerpt": post_summary.excerpt, "url": resolved_url, "published_at": post_summary.published_at.isoformat() if post_summary.published_at else None, "updated_at": post_summary.updated_at.isoformat() if post_summary.updated_at else None, "tags": post_summary.tags, "authors": post_summary.authors, "markdown": markdown, } return { "status": {"code": 200, "text": "OK"}, "headers": {"Content-Type": "application/json", "x-resolved-url": resolved_url}, "body": {"kind": "text", "text": json.dumps(response_body)}, } - src/mcp_server.py:82-82 (registration)Registers the 'fetch' tool with FastMCP using the @mcp.tool decorator, specifying the name 'fetch'.
@mcp.tool(name="fetch") - src/mcp_server.py:33-59 (helper)Helper function to parse various input formats (post://slug, full URLs, bare slugs) into a post slug, used in the fetch handler.
def _extract_slug_from_url(url: str) -> str | None: """Extract a post slug from user input. The MCP contract references HTTP-style requests, so we support a few shapes: - ``post://{slug}`` or ``ghost://{slug}`` custom schemes - Fully qualified Ghost URLs (``https://example.com/posts/{slug}``) - Bare slugs with no scheme """ parsed = urlparse(url) if parsed.scheme in {"post", "ghost", ""}: if parsed.path and parsed.path.strip("/"): return parsed.path.strip("/") if parsed.netloc: return parsed.netloc return parsed.path or None if parsed.scheme in {"http", "https"}: path = parsed.path.strip("/") if not path: return None return path.split("/")[-1] return None - src/mcp_server.py:61-80 (helper)Helper function to construct or resolve the canonical HTTPS URL for a post, using post.url, fallback, or base + slug, used in fetch.
def _canonical_post_url(post_summary: PostSummary, fallback_url: str | None = None) -> str | None: """Resolve a canonical, HTTP(S) URL for a post.""" if post_summary.url: return post_summary.url if fallback_url: parsed_fallback = urlparse(fallback_url) if parsed_fallback.scheme in {"http", "https"} and parsed_fallback.netloc: return fallback_url base_url = settings.ghost_api_url if base_url and post_summary.slug: parsed_base = urlparse(base_url) if parsed_base.scheme and parsed_base.netloc: origin = f"{parsed_base.scheme}://{parsed_base.netloc}" return urljoin(origin.rstrip("/") + "/", post_summary.slug.strip("/")) return None