diff --git a/.gitignore b/.gitignore index 3ecb537..aa03da0 100644 --- a/.gitignore +++ b/.gitignore @@ -26,5 +26,10 @@ htmlcov/ # build artifact from scripts/build-skill.sh /dist/ +# Go MCP bundle build outputs - source of truth for vendored/ stays under +# skills/last30days/scripts/; build/ holds cross-compiled binaries + .mcpb files. +/mcp/vendored/ +/mcp/build/ + # Internal planning docs (ce:plan output) — keep local, don't publish docs/plans/ diff --git a/mcp/.gitignore b/mcp/.gitignore new file mode 100644 index 0000000..3852985 --- /dev/null +++ b/mcp/.gitignore @@ -0,0 +1,9 @@ +# Mirror of skills/last30days/scripts/, populated by scripts/sync-engine.sh. +# Source of truth lives in the Python skill; never commit the mirror. +vendored/ + +# Local build output: cross-compiled binaries and packaged .mcpb files. +build/ +# Anchor to the mcp/ root so the cmd/last30days-pp-mcp/ package directory +# is not also excluded (subdirs with the same name would otherwise match). +/last30days-pp-mcp diff --git a/mcp/README.md b/mcp/README.md new file mode 100644 index 0000000..72a5f48 --- /dev/null +++ b/mcp/README.md @@ -0,0 +1,36 @@ +# last30days-pp-mcp + +Go MCP server that wraps the last30days Python engine for Claude Desktop. Packaged as a `.mcpb` bundle (drag-drop install into Claude Desktop). + +The MCP server exposes a single `research` tool that mirrors the `/last30days ` slash command available in Claude Code. At runtime the binary extracts the vendored Python engine into a per-user cache and shells out to `python3` to produce the synthesis input Claude renders. + +## Architecture + +- `cmd/last30days-pp-mcp/` - server entry point +- `internal/engine/` - `embed.FS` of the Python engine + cache extractor + subprocess wrapper +- `internal/tools/` - MCP tool handlers (currently `research`) +- `vendored/` - mirror of `skills/last30days/scripts/`, generated by `scripts/sync-engine.sh` (gitignored) +- `manifest.json` - MCPB v0.3 manifest consumed by Claude Desktop and `printing-press bundle` + +## Local build + +```bash +# Mirror the Python engine into vendored/. +bash scripts/sync-engine.sh + +# Build for the current host. +go build -ldflags "-X main.Version=dev" -o build/last30days-pp-mcp ./cmd/last30days-pp-mcp + +# Package as a .mcpb (requires the printing-press binary on PATH). +printing-press bundle . --skip-build --binary build/last30days-pp-mcp +``` + +The output `.mcpb` lands at `build/last30days-pp-mcp--.mcpb`. Drag it into Claude Desktop's Extensions panel to install. + +## Runtime requirements + +End users need Python 3.12+ on PATH. The bundle ships the engine source but relies on the host interpreter. + +## Versioning + +The MCPB `manifest.json` version is hand-bumped in the same PR that ships engine changes worth releasing. Release CI stamps the Go binary's `main.Version` from the tag. diff --git a/mcp/cmd/last30days-pp-mcp/main.go b/mcp/cmd/last30days-pp-mcp/main.go new file mode 100644 index 0000000..f6158cd --- /dev/null +++ b/mcp/cmd/last30days-pp-mcp/main.go @@ -0,0 +1,11 @@ +// Package main is the entry point for the last30days MCP server bundled +// as a .mcpb for Claude Desktop. See mcp/README.md for build instructions. +package main + +// Version is stamped at build time via -ldflags "-X main.Version=". +// Defaults to "dev" for local builds. +var Version = "dev" + +func main() { + // Wired in U3. +} diff --git a/mcp/go.mod b/mcp/go.mod new file mode 100644 index 0000000..dd5b944 --- /dev/null +++ b/mcp/go.mod @@ -0,0 +1,3 @@ +module github.com/mvanhorn/last30days-skill/mcp + +go 1.22 diff --git a/mcp/scripts/sync-engine.sh b/mcp/scripts/sync-engine.sh new file mode 100755 index 0000000..d0b51d6 --- /dev/null +++ b/mcp/scripts/sync-engine.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Mirrors skills/last30days/scripts/{last30days.py,lib/} into mcp/vendored/ +# so the Go binary's embed.FS captures the engine at build time. +# +# Source of truth: skills/last30days/scripts/. Never edit mcp/vendored/ directly. +# Run before `go build` locally and in CI before `printing-press bundle`. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MCP_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +REPO_ROOT="$(cd "${MCP_DIR}/.." && pwd)" +ENGINE_SRC="${REPO_ROOT}/skills/last30days/scripts" +VENDORED="${MCP_DIR}/vendored" + +if [ ! -f "${ENGINE_SRC}/last30days.py" ]; then + echo "sync-engine: ${ENGINE_SRC}/last30days.py not found" >&2 + exit 1 +fi + +rm -rf "${VENDORED}" +mkdir -p "${VENDORED}" + +# Copy the entry script and the lib/ tree (modules + lib/vendor/). +cp "${ENGINE_SRC}/last30days.py" "${VENDORED}/last30days.py" +cp -R "${ENGINE_SRC}/lib" "${VENDORED}/lib" + +# Strip caches so the embed.FS stays deterministic. +find "${VENDORED}" -type d -name "__pycache__" -prune -exec rm -rf {} + +find "${VENDORED}" -type f -name "*.pyc" -delete + +echo "sync-engine: vendored engine at ${VENDORED}"