mirror of
https://github.com/himanshudongre/smriti.git
synced 2026-08-28 05:14:59 +00:00
688 lines
25 KiB
Python
688 lines
25 KiB
Python
"""Smriti MCP server — stdio transport.
|
|
|
|
Exposes the Smriti CLI surface as 12 MCP tools so agents inside MCP-aware
|
|
hosts (Claude Code, Cursor, Windsurf) can read and write reasoning state
|
|
without shelling out to the `smriti` binary.
|
|
|
|
Thin shim: each tool builds a SmritiClient, calls 1-2 client methods,
|
|
runs the result through an existing formatter, returns a str. FastMCP
|
|
auto-wraps the string into TextContent. Errors raise SmritiToolError
|
|
which FastMCP converts into an MCP error response.
|
|
|
|
Entry point: `smriti-mcp` (see cli/pyproject.toml [project.scripts]).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from typing import Any
|
|
|
|
from mcp.server.fastmcp import FastMCP
|
|
|
|
from .client import SmritiClient, SmritiError
|
|
from .formatters import (
|
|
format_checkpoint,
|
|
format_commit_list,
|
|
format_compare_result,
|
|
format_fork_result,
|
|
format_restore_brief,
|
|
format_review,
|
|
format_space_list,
|
|
format_state_brief,
|
|
)
|
|
|
|
|
|
# ── server instance ─────────────────────────────────────────────────────
|
|
|
|
mcp = FastMCP("smriti")
|
|
|
|
|
|
# ── client factory ──────────────────────────────────────────────────────
|
|
|
|
|
|
def _client() -> SmritiClient:
|
|
"""Build a fresh SmritiClient per tool call.
|
|
|
|
Constructing per-call is cheap (requests.Session reuse is nice-to-have
|
|
but not critical) and lets SMRITI_API_URL env changes take effect
|
|
without restarting the MCP server subprocess.
|
|
"""
|
|
return SmritiClient(base_url=os.environ.get("SMRITI_API_URL"))
|
|
|
|
|
|
# ── error wrapper ───────────────────────────────────────────────────────
|
|
|
|
|
|
class SmritiToolError(Exception):
|
|
"""Raised from a tool handler to signal a user-visible error to the
|
|
MCP host. FastMCP catches exceptions raised inside tool functions and
|
|
converts them into MCP error responses on the tool call.
|
|
|
|
We raise this instead of SmritiError so the message prefix is
|
|
predictable and includes the HTTP status + structured detail when
|
|
present.
|
|
"""
|
|
|
|
|
|
def _raise_from(err: SmritiError) -> None:
|
|
"""Convert a SmritiError into a SmritiToolError with consistent
|
|
formatting. Includes HTTP status, message, and structured detail
|
|
(pretty-printed JSON) if present. Always raises."""
|
|
pieces: list[str] = []
|
|
if err.status is not None:
|
|
pieces.append(f"HTTP {err.status}")
|
|
pieces.append(str(err))
|
|
if isinstance(err.detail, dict):
|
|
pieces.append(json.dumps(err.detail, indent=2, default=str))
|
|
raise SmritiToolError("\n".join(pieces))
|
|
|
|
|
|
# ── tools ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
@mcp.tool()
|
|
def smriti_list_spaces() -> str:
|
|
"""List all Smriti spaces (projects).
|
|
|
|
Returns a markdown bullet list of every space's name, UUID, and
|
|
description. Use this to orient on what projects exist before calling
|
|
smriti_state or smriti_list_checkpoints.
|
|
"""
|
|
try:
|
|
spaces = _client().list_spaces()
|
|
except SmritiError as e:
|
|
_raise_from(e)
|
|
return format_space_list(spaces)
|
|
|
|
|
|
def _empty_space_brief(space: dict) -> str:
|
|
"""Short message returned when a space exists but has no checkpoints.
|
|
Shared between the default and main_only paths of smriti_state so
|
|
both paths produce the same output on empty spaces."""
|
|
lines = [f"# {space.get('name', 'Untitled space')}"]
|
|
if space.get("description"):
|
|
lines.append("")
|
|
lines.append(space["description"])
|
|
lines.append("")
|
|
lines.append(
|
|
"No checkpoints yet. Create one with smriti_create_checkpoint."
|
|
)
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
@mcp.tool()
|
|
def smriti_state(
|
|
space: str,
|
|
preview: bool = False,
|
|
compact: bool = False,
|
|
main_only: bool = False,
|
|
) -> str:
|
|
"""Print a continuation-oriented brief of a space's current state.
|
|
|
|
By default the brief is multi-branch aware: it includes the main-branch
|
|
continuation context (objective, summary, decisions, assumptions, open
|
|
questions, tasks, artifacts) PLUS a concise "Active branches" section
|
|
listing up to 5 non-main branches with recent activity, PLUS a
|
|
lightweight "Divergence signal" section when any active branch
|
|
disagrees with main on decisions. Both extensions are elided cleanly
|
|
when the project has no fork activity, so single-agent projects see
|
|
exactly the same output as before.
|
|
|
|
This is the single best tool for "what's this project about right
|
|
now across all agents working on it" — call it at the start of any
|
|
session where you need to pick up where work left off.
|
|
|
|
Args:
|
|
space: Space name or UUID.
|
|
preview: If True, truncate artifact content to a short preview
|
|
instead of showing it in full. Default: full artifacts.
|
|
compact: If True, omit artifact content entirely — show only
|
|
artifact labels with a recovery instruction. Saves tokens
|
|
for session-start injection. Full content recoverable via
|
|
smriti_show_checkpoint. Default False.
|
|
main_only: If True, fetch only the main-branch HEAD via the
|
|
legacy /head endpoint and skip the Active branches and
|
|
Divergence signal sections entirely. Default False.
|
|
"""
|
|
client = _client()
|
|
try:
|
|
s = client.resolve_space(space)
|
|
if main_only:
|
|
head = client.get_head(s["id"])
|
|
if not head.get("commit_id"):
|
|
return _empty_space_brief(s)
|
|
commit = client.get_commit(head["commit_id"])
|
|
space_state: dict | None = None
|
|
else:
|
|
state = client.get_space_state(s["id"])
|
|
head = state.get("head") or {}
|
|
commit = state.get("commit") or {}
|
|
if not head.get("commit_id"):
|
|
return _empty_space_brief(state.get("space") or s)
|
|
space_state = {
|
|
"active_branches": state.get("active_branches") or [],
|
|
"active_claims": state.get("active_claims") or [],
|
|
"divergence": state.get("divergence"),
|
|
}
|
|
except SmritiError as e:
|
|
_raise_from(e)
|
|
return format_state_brief(
|
|
s, head, commit,
|
|
full_artifacts=not preview and not compact,
|
|
compact=compact,
|
|
space_state=space_state,
|
|
)
|
|
|
|
|
|
@mcp.tool()
|
|
def smriti_show_checkpoint(checkpoint_id: str, full_artifacts: bool = False) -> str:
|
|
"""Print a specific checkpoint as markdown.
|
|
|
|
Fetches the checkpoint by UUID and renders every field (message,
|
|
objective, summary, decisions, assumptions, tasks, open questions,
|
|
entities, artifacts) as a detail view. Use this when you already
|
|
know the checkpoint you want to inspect — for a space-level brief
|
|
of the current HEAD, use smriti_state instead.
|
|
|
|
Args:
|
|
checkpoint_id: Checkpoint UUID.
|
|
full_artifacts: If True, include full artifact content.
|
|
Default: truncated preview.
|
|
"""
|
|
try:
|
|
commit = _client().get_commit(checkpoint_id)
|
|
except SmritiError as e:
|
|
_raise_from(e)
|
|
return format_checkpoint(commit, full_artifacts=full_artifacts)
|
|
|
|
|
|
@mcp.tool()
|
|
def smriti_list_checkpoints(space: str, branch: str = "") -> str:
|
|
"""List checkpoints in a space, optionally filtered by branch.
|
|
|
|
Returns a markdown list with each checkpoint's short hash, message,
|
|
creation time, and branch annotation (for non-main branches). Use
|
|
this to navigate a project's history before picking a specific
|
|
checkpoint for smriti_show_checkpoint, smriti_compare, or
|
|
smriti_restore.
|
|
|
|
Args:
|
|
space: Space name or UUID.
|
|
branch: Optional branch name filter (e.g. "alternative-design").
|
|
Empty = all branches.
|
|
"""
|
|
client = _client()
|
|
try:
|
|
s = client.resolve_space(space)
|
|
commits = client.list_commits(s["id"], branch=branch or None)
|
|
except SmritiError as e:
|
|
_raise_from(e)
|
|
return format_commit_list(commits)
|
|
|
|
|
|
@mcp.tool()
|
|
def smriti_review_checkpoint(checkpoint_id: str) -> str:
|
|
"""Run the LLM-backed consistency review on a checkpoint.
|
|
|
|
Sends the checkpoint's structured fields to Smriti's background
|
|
intelligence provider and returns a short list of potential issues
|
|
(contradictions, hidden assumptions, possibly-resolved questions,
|
|
unused entities) plus actionable suggestions. Use this as a
|
|
self-audit before handing a checkpoint off to another agent.
|
|
|
|
Args:
|
|
checkpoint_id: Checkpoint UUID.
|
|
"""
|
|
try:
|
|
result = _client().review_checkpoint(checkpoint_id)
|
|
except SmritiError as e:
|
|
_raise_from(e)
|
|
return format_review(result)
|
|
|
|
|
|
@mcp.tool()
|
|
def smriti_restore(checkpoint_id: str, preview: bool = False) -> str:
|
|
"""Print a specific checkpoint as a continuation brief.
|
|
|
|
Renders the checkpoint in the same shape as smriti_state but
|
|
targeted at an arbitrary checkpoint rather than the space's HEAD.
|
|
The header explicitly marks this as a restore brief so callers
|
|
can distinguish it from a live state snapshot. Use this to pull
|
|
an older or branched checkpoint into the session context as if
|
|
you were continuing from it.
|
|
|
|
Args:
|
|
checkpoint_id: Checkpoint UUID.
|
|
preview: If True, truncate artifact content to a short preview.
|
|
Default: full artifacts.
|
|
"""
|
|
client = _client()
|
|
try:
|
|
commit = client.get_commit(checkpoint_id)
|
|
space = client.get_space(str(commit.get("repo_id", "")))
|
|
except SmritiError as e:
|
|
_raise_from(e)
|
|
return format_restore_brief(space, commit, full_artifacts=not preview)
|
|
|
|
|
|
@mcp.tool()
|
|
def smriti_compare(
|
|
checkpoint_a: str,
|
|
checkpoint_b: str,
|
|
full_artifacts: bool = False,
|
|
) -> str:
|
|
"""Compare two checkpoints and return a structured diff.
|
|
|
|
Shows the common ancestor, per-side summaries and objectives, and
|
|
Shared / Only-in-A / Only-in-B splits for decisions, assumptions,
|
|
and tasks. Shared-set matching is case- and punctuation-insensitive
|
|
so two checkpoints phrasing the same commitment differently still
|
|
show up as shared. Use this to review how a forked branch has
|
|
diverged from main, or to see two alternative design directions
|
|
side-by-side.
|
|
|
|
Args:
|
|
checkpoint_a: First checkpoint UUID (side A).
|
|
checkpoint_b: Second checkpoint UUID (side B).
|
|
full_artifacts: Accepted for interface consistency; not currently
|
|
used (compare does not surface artifacts).
|
|
"""
|
|
try:
|
|
result = _client().compare_checkpoints(checkpoint_a, checkpoint_b)
|
|
except SmritiError as e:
|
|
_raise_from(e)
|
|
return format_compare_result(result, full_artifacts=full_artifacts)
|
|
|
|
|
|
@mcp.tool()
|
|
def smriti_create_space(name: str, description: str = "") -> str:
|
|
"""Create a new Smriti space (project).
|
|
|
|
Spaces are the top-level container for reasoning state. Every
|
|
checkpoint lives in exactly one space, and sessions are scoped to
|
|
a space.
|
|
|
|
Args:
|
|
name: The space's display name. Does not need to be globally
|
|
unique but `smriti_state`/`smriti_list_checkpoints` prefer
|
|
unique names so they can resolve by name rather than UUID.
|
|
description: Optional short description.
|
|
"""
|
|
try:
|
|
space = _client().create_space(name, description)
|
|
except SmritiError as e:
|
|
_raise_from(e)
|
|
return f"Created space `{space['id']}`: {space['name']}\n"
|
|
|
|
|
|
@mcp.tool()
|
|
def smriti_fork(checkpoint_id: str, branch: str = "") -> str:
|
|
"""Fork a new session from an existing checkpoint.
|
|
|
|
Creates a new chat session seeded from the given checkpoint on a
|
|
new branch. The first checkpoint written to the new session (via
|
|
smriti_create_checkpoint with `session=<fork-session-id>`) will
|
|
have the fork source as its parent. Use this to explore an
|
|
alternative direction from a checkpoint without losing the main
|
|
branch.
|
|
|
|
Args:
|
|
checkpoint_id: UUID of the checkpoint to fork from.
|
|
branch: Optional branch name for the new session. If empty,
|
|
the backend generates a dated branch name like
|
|
"branch-2026-04-12".
|
|
"""
|
|
client = _client()
|
|
try:
|
|
commit = client.get_commit(checkpoint_id)
|
|
fork = client.fork_session(
|
|
space_id=str(commit.get("repo_id", "")),
|
|
checkpoint_id=checkpoint_id,
|
|
branch_name=branch or "",
|
|
)
|
|
except SmritiError as e:
|
|
_raise_from(e)
|
|
return format_fork_result(fork, commit)
|
|
|
|
|
|
@mcp.tool()
|
|
def smriti_create_checkpoint(
|
|
space: str,
|
|
content: str,
|
|
session: str = "",
|
|
author_agent: str = "",
|
|
project_root: str = "",
|
|
dry_run: bool = False,
|
|
) -> str:
|
|
"""Create a checkpoint in the given space from freeform markdown.
|
|
|
|
The `content` is sent to Smriti's extract endpoint which uses a
|
|
background LLM to pull out structured fields (message, objective,
|
|
summary, decisions, assumptions, tasks, open_questions, entities,
|
|
artifacts). If `dry_run=True`, the extracted payload is returned as
|
|
JSON inside a fenced code block without creating a checkpoint — use
|
|
this to preview the extractor's output before committing.
|
|
|
|
This is the primary tool for writing state into Smriti from an
|
|
agent session: write a freeform markdown document describing what
|
|
you figured out, pass it here, and the LLM does the structuring.
|
|
|
|
Args:
|
|
space: Space name or UUID.
|
|
content: Freeform markdown describing the work (a design doc,
|
|
handoff note, session summary, etc.). Must be non-empty.
|
|
session: Optional existing session UUID to attach the checkpoint
|
|
to (used for fork workflows — pair with `smriti_fork`).
|
|
Empty = create a new lightweight session under the space.
|
|
author_agent: Optional agent identifier to tag on the checkpoint
|
|
(e.g. "claude-code", "codex-local"). Empty = backend
|
|
fallback to the session's active provider.
|
|
project_root: Optional absolute path to record as the
|
|
checkpoint's project_root. Empty = no project_root
|
|
recorded. Unlike the CLI, the MCP server does NOT auto-
|
|
capture cwd, because MCP servers run in the host's arbitrary
|
|
working directory. Pass an explicit path when you want the
|
|
checkpoint to know where the project lives on disk.
|
|
dry_run: If True, return the extracted payload as JSON without
|
|
creating a checkpoint.
|
|
"""
|
|
if not content.strip():
|
|
raise SmritiToolError("content must be a non-empty markdown string.")
|
|
|
|
client = _client()
|
|
try:
|
|
space_dict = client.resolve_space(space)
|
|
extracted = client.extract_checkpoint_content(content)
|
|
payload: dict[str, Any] = {
|
|
"message": (extracted.get("title") or "").strip() or "Extracted checkpoint",
|
|
"objective": extracted.get("objective", ""),
|
|
"summary": extracted.get("summary", ""),
|
|
"decisions": extracted.get("decisions", []),
|
|
"assumptions": extracted.get("assumptions", []),
|
|
"tasks": extracted.get("tasks", []),
|
|
"open_questions": extracted.get("open_questions", []),
|
|
"entities": extracted.get("entities", []),
|
|
"artifacts": extracted.get("artifacts", []),
|
|
}
|
|
|
|
if dry_run:
|
|
return (
|
|
"Dry run — extracted payload (no checkpoint created):\n\n"
|
|
"```json\n"
|
|
+ json.dumps(payload, indent=2, default=str)
|
|
+ "\n```\n"
|
|
)
|
|
|
|
if session:
|
|
session_id = session
|
|
else:
|
|
sess = client.create_session(
|
|
repo_id=space_dict["id"],
|
|
title=f"mcp: {payload['message'][:80]}",
|
|
)
|
|
session_id = sess["id"]
|
|
|
|
commit_payload: dict[str, Any] = {
|
|
"repo_id": space_dict["id"],
|
|
"session_id": session_id,
|
|
**payload,
|
|
}
|
|
if project_root:
|
|
commit_payload["project_root"] = project_root
|
|
if author_agent:
|
|
commit_payload["author_agent"] = author_agent
|
|
|
|
commit = client.create_chat_commit(commit_payload)
|
|
except SmritiError as e:
|
|
_raise_from(e)
|
|
|
|
short = (commit.get("commit_hash") or "")[:7]
|
|
msg = commit.get("message", "")
|
|
branch = commit.get("branch_name") or "main"
|
|
return f"Created checkpoint `{short}` on branch `{branch}`: {msg}\n"
|
|
|
|
|
|
@mcp.tool()
|
|
def smriti_delete_space(space: str) -> str:
|
|
"""Delete a Smriti space and everything it contains.
|
|
|
|
Cascades to every checkpoint, session, and turn under the space.
|
|
This is irreversible. The MCP host's tool-approval gate is the
|
|
only safety check — there is no per-tool confirmation prompt
|
|
(unlike the CLI's `-y` flag, because the MCP tool-call approval
|
|
already serves that purpose).
|
|
|
|
Args:
|
|
space: Space name or UUID.
|
|
"""
|
|
client = _client()
|
|
try:
|
|
s = client.resolve_space(space)
|
|
commits = client.list_commits(s["id"])
|
|
client.delete_space(s["id"])
|
|
except SmritiError as e:
|
|
_raise_from(e)
|
|
return f"Deleted space '{s['name']}' and its {len(commits)} checkpoint(s).\n"
|
|
|
|
|
|
@mcp.tool()
|
|
def smriti_close_branch(
|
|
space: str,
|
|
branch: str,
|
|
disposition: str = "integrated",
|
|
) -> str:
|
|
"""Mark a branch as integrated, abandoned, or active.
|
|
|
|
Branches marked integrated or abandoned stop appearing in the
|
|
## Active branches and ## Divergence signal sections of
|
|
smriti_state. Their checkpoints remain in the lineage tree for
|
|
history — nothing is deleted.
|
|
|
|
Setting back to active re-shows the branch. Fully reversible.
|
|
|
|
Call this as part of your clean-finish workflow after a branch's
|
|
work has been merged or intentionally stopped.
|
|
|
|
Args:
|
|
space: Space name or UUID.
|
|
branch: Branch name to update.
|
|
disposition: "integrated", "abandoned", or "active".
|
|
"""
|
|
client = _client()
|
|
try:
|
|
s = client.resolve_space(space)
|
|
result = client.close_branch(s["id"], branch, disposition)
|
|
except SmritiError as e:
|
|
_raise_from(e)
|
|
return (
|
|
f"Branch `{result['branch_name']}` marked `{result['disposition']}` "
|
|
f"({result['sessions_updated']} session(s) updated)."
|
|
)
|
|
|
|
|
|
@mcp.tool()
|
|
def smriti_claim(
|
|
space: str,
|
|
scope: str,
|
|
agent: str = "claude-code",
|
|
branch: str = "main",
|
|
intent_type: str = "implement",
|
|
ttl_hours: float = 4.0,
|
|
) -> str:
|
|
"""Declare a work claim before starting substantial work.
|
|
|
|
A work claim makes your intent visible to other agents via
|
|
`smriti_state`. Other agents seeing your active claim will know
|
|
you are already working on this scope and can avoid collision.
|
|
|
|
Create a claim AFTER reading state and reconciling, but BEFORE
|
|
writing code. Mark it done with `smriti_claim_done` when finished.
|
|
|
|
Claims are advisory — not locks. They expire after `ttl_hours`
|
|
if not explicitly resolved.
|
|
|
|
Args:
|
|
space: Space name or UUID.
|
|
scope: One sentence describing what you are about to work on.
|
|
agent: Your agent identifier (default: claude-code).
|
|
branch: Branch you will work on (default: main).
|
|
intent_type: One of: implement, review, investigate, docs, test.
|
|
ttl_hours: Hours until the claim expires (default: 4).
|
|
"""
|
|
client = _client()
|
|
try:
|
|
s = client.resolve_space(space)
|
|
head = client.get_head(s["id"])
|
|
base_commit_id = head.get("commit_id")
|
|
claim = client.create_claim(
|
|
space_id=s["id"],
|
|
agent=agent,
|
|
scope=scope,
|
|
branch_name=branch,
|
|
base_commit_id=base_commit_id,
|
|
intent_type=intent_type,
|
|
ttl_hours=ttl_hours,
|
|
)
|
|
except SmritiError as e:
|
|
_raise_from(e)
|
|
return (
|
|
f"Claimed: [{claim['intent_type']}] \"{claim['scope']}\" "
|
|
f"on `{claim['branch_name']}` by `{claim['agent']}`\n\n"
|
|
f"Claim ID: `{claim['id']}`\n"
|
|
f"Expires in {ttl_hours}h. Mark done with "
|
|
f"`smriti_claim_done(claim_id=\"{claim['id']}\")`."
|
|
)
|
|
|
|
|
|
@mcp.tool()
|
|
def smriti_claim_done(claim_id: str, abandon: bool = False) -> str:
|
|
"""Mark a work claim as done or abandoned.
|
|
|
|
Call this when you finish the work declared in the claim. If the
|
|
work was not completed (e.g. you were blocked or changed direction),
|
|
pass `abandon=True` instead.
|
|
|
|
Args:
|
|
claim_id: The claim UUID returned by smriti_claim.
|
|
abandon: If True, mark as abandoned instead of done.
|
|
"""
|
|
client = _client()
|
|
status = "abandoned" if abandon else "done"
|
|
try:
|
|
claim = client.update_claim(claim_id, status)
|
|
except SmritiError as e:
|
|
_raise_from(e)
|
|
return f"Claim `{claim['id']}` marked {claim['status']}."
|
|
|
|
|
|
@mcp.tool()
|
|
def smriti_install_skill(target: str) -> str:
|
|
"""Return the Smriti agent skill pack for an agent target.
|
|
|
|
The skill pack is a versioned markdown file that teaches the
|
|
agent when and why to use Smriti's tools — when to checkpoint
|
|
(and critically, when NOT to checkpoint), when to fork, when to
|
|
review, how to detect drift, and the explicit anti-patterns to
|
|
reject (including "don't write HANDOFF.md when a Smriti space
|
|
exists"). Treat this as onboarding for the agent — installing it
|
|
into the project's host should make Smriti usage reflexive
|
|
rather than something the agent has to reason through each
|
|
call.
|
|
|
|
IMPORTANT: unlike the CLI install command, this MCP tool does
|
|
NOT write any files. The MCP server runs in the host's
|
|
arbitrary working directory and has no business planting files
|
|
on disk without the host's file-system tool approval. The tool
|
|
returns the rendered markdown inside a fenced code block; the
|
|
agent is expected to read the suggested destination from the
|
|
tool's output and write the file using its host's own file
|
|
tools (Edit, Write, Bash, etc.). This keeps the MCP server
|
|
read-only from the host's perspective.
|
|
|
|
The same content renders for each target — only the primary
|
|
tool notation differs (MCP tool calls for claude-code, shell
|
|
commands for codex).
|
|
|
|
Args:
|
|
target: "claude-code" or "codex".
|
|
"""
|
|
from .skill_pack import get_target, render
|
|
|
|
try:
|
|
target_obj = get_target(target)
|
|
content = render(target)
|
|
except ValueError as e:
|
|
raise SmritiToolError(str(e))
|
|
|
|
return (
|
|
f"# Smriti skill pack for {target_obj.display_name}\n\n"
|
|
f"**Suggested destination (relative to project root):** "
|
|
f"`{target_obj.default_destination}`\n\n"
|
|
f"Write the markdown block below to that path using your "
|
|
f"host's file tools. Re-run this tool whenever the skill "
|
|
f"pack version changes.\n\n"
|
|
f"```markdown\n{content}\n```\n"
|
|
)
|
|
|
|
|
|
@mcp.tool()
|
|
def smriti_delete_checkpoint(checkpoint_id: str, cascade: bool = False) -> str:
|
|
"""Delete a checkpoint.
|
|
|
|
Refuses with a descriptive error when the checkpoint has child
|
|
commits or forked sessions, unless `cascade=True` is passed. The
|
|
error message lists the blocking dependents (child commits and
|
|
forked session branches) and reminds the caller to retry with
|
|
`cascade=True` if they really want to delete the whole subtree.
|
|
|
|
Args:
|
|
checkpoint_id: Checkpoint UUID.
|
|
cascade: If True, also delete descendant commits and forked
|
|
sessions. Default False refuses on any blocking dependent.
|
|
"""
|
|
client = _client()
|
|
try:
|
|
commit = client.get_commit(checkpoint_id)
|
|
except SmritiError as e:
|
|
_raise_from(e)
|
|
|
|
try:
|
|
client.delete_commit(checkpoint_id, cascade=cascade)
|
|
except SmritiError as e:
|
|
if e.status == 409 and isinstance(e.detail, dict):
|
|
deps = e.detail.get("dependents", {}) or {}
|
|
lines = [f"Refusing to delete: {e.detail.get('message', str(e))}"]
|
|
for c in deps.get("child_commits", []):
|
|
lines.append(f" - child commit: {c.get('label', '?')} ({c.get('id', '?')})")
|
|
for s in deps.get("forked_sessions", []):
|
|
lines.append(f" - forked session: {s.get('label', '?')} ({s.get('id', '?')})")
|
|
lines.append(" Re-run with cascade=true to delete the subtree.")
|
|
raise SmritiToolError("\n".join(lines))
|
|
_raise_from(e)
|
|
|
|
short = (commit.get("commit_hash") or "")[:7]
|
|
note = " (cascade)" if cascade else ""
|
|
return f"Deleted checkpoint `{short}`{note}.\n"
|
|
|
|
|
|
# ── entry point ─────────────────────────────────────────────────────────
|
|
|
|
|
|
def main() -> None:
|
|
"""Entry point for the `smriti-mcp` console script. Runs over stdio.
|
|
|
|
The `mcp` SDK emits an INFO line ("Processing request of type …") on
|
|
every tool call, which clutters host log panels during normal use.
|
|
Default the `mcp` logger to WARNING; set `SMRITI_MCP_LOG_LEVEL=INFO`
|
|
(or DEBUG) to re-enable verbose logging when debugging a transport
|
|
issue.
|
|
"""
|
|
import logging
|
|
|
|
log_level = os.environ.get("SMRITI_MCP_LOG_LEVEL", "WARNING").upper()
|
|
logging.getLogger("mcp").setLevel(log_level)
|
|
mcp.run(transport="stdio")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|