Ship a Claude Code plugin with the haiku-rag skill
claude-plugin/ holds the plugin manifest, the server configuration (haiku-rag mcp --stdio, the configuration decides the database) and a skill that says when to reach for the knowledge base and how to move from a search result to a document, a section, an answer or a computation. A repo-root marketplace manifest makes `claude plugin marketplace add ggozad/haiku.rag` work. The skill pre-approves every tool the server registers, and a test keeps the two in step. The manifest carries the package version, which bump_version.py now rewrites: a versioned plugin updates only on a bump, so the installed skill stays in step with the haiku-rag release the user has. Refs #599
This commit is contained in:
parent
f45ed90b33
commit
eb9995e9b7
10 changed files with 231 additions and 3 deletions
14
.claude-plugin/marketplace.json
Normal file
14
.claude-plugin/marketplace.json
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"name": "haiku-rag",
|
||||||
|
"description": "The haiku.rag knowledge base as Claude Code tools and a skill.",
|
||||||
|
"owner": {
|
||||||
|
"name": "Yiorgis Gozadinos"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "haiku-rag",
|
||||||
|
"source": "./claude-plugin",
|
||||||
|
"description": "Search, read and question your haiku.rag knowledge base from Claude Code."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,9 @@
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Claude Code plugin under `claude-plugin/`: the server configuration and the
|
||||||
|
`haiku-rag` skill. `claude plugin marketplace add ggozad/haiku.rag`, then
|
||||||
|
`claude plugin install haiku-rag`.
|
||||||
- `haiku-rag mcp --no-agents` leaves `ask_question` and `analyze`
|
- `haiku-rag mcp --no-agents` leaves `ask_question` and `analyze`
|
||||||
unregistered. `create_mcp_server(agents=)`, `HaikuRAGApp.run_mcp(agents=)`.
|
unregistered. `create_mcp_server(agents=)`, `HaikuRAGApp.run_mcp(agents=)`.
|
||||||
- MCP tools `get_document_outline` (heading tree with page numbers) and
|
- MCP tools `get_document_outline` (heading tree with page numbers) and
|
||||||
|
|
|
||||||
|
|
@ -110,12 +110,19 @@ For direct agent composition, see the [capabilities documentation](https://ggoza
|
||||||
|
|
||||||
## MCP Server
|
## MCP Server
|
||||||
|
|
||||||
Use with AI assistants like Claude Desktop:
|
Use with AI assistants like Claude Code and Claude Desktop:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
haiku-rag mcp --stdio
|
haiku-rag mcp --stdio
|
||||||
```
|
```
|
||||||
|
|
||||||
|
In Claude Code, install the plugin, which registers the server and a skill:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude plugin marketplace add ggozad/haiku.rag
|
||||||
|
claude plugin install haiku-rag
|
||||||
|
```
|
||||||
|
|
||||||
Add to your Claude Desktop configuration:
|
Add to your Claude Desktop configuration:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|
|
||||||
12
claude-plugin/.claude-plugin/plugin.json
Normal file
12
claude-plugin/.claude-plugin/plugin.json
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"name": "haiku-rag",
|
||||||
|
"version": "0.82.1",
|
||||||
|
"description": "Search, read and question your haiku.rag knowledge base from Claude Code.",
|
||||||
|
"author": {
|
||||||
|
"name": "Yiorgis Gozadinos",
|
||||||
|
"email": "ggozadinos@gmail.com"
|
||||||
|
},
|
||||||
|
"homepage": "https://ggozad.github.io/haiku.rag/mcp/",
|
||||||
|
"repository": "https://github.com/ggozad/haiku.rag",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
8
claude-plugin/.mcp.json
Normal file
8
claude-plugin/.mcp.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"haiku-rag": {
|
||||||
|
"command": "haiku-rag",
|
||||||
|
"args": ["mcp", "--stdio"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
65
claude-plugin/skills/haiku-rag/SKILL.md
Normal file
65
claude-plugin/skills/haiku-rag/SKILL.md
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
---
|
||||||
|
name: haiku-rag
|
||||||
|
description: Search, read and question the user's haiku.rag knowledge base
|
||||||
|
through the haiku-rag MCP tools. Use whenever a request could be answered
|
||||||
|
from the user's ingested documents, when asked to find, look up, check or
|
||||||
|
cite something in their documents or knowledge base, or when the question is
|
||||||
|
about the user's own material rather than general knowledge.
|
||||||
|
allowed-tools:
|
||||||
|
- mcp__plugin_haiku-rag_haiku-rag__search_documents
|
||||||
|
- mcp__plugin_haiku-rag_haiku-rag__search_documents_by_image
|
||||||
|
- mcp__plugin_haiku-rag_haiku-rag__get_document
|
||||||
|
- mcp__plugin_haiku-rag_haiku-rag__get_document_outline
|
||||||
|
- mcp__plugin_haiku-rag_haiku-rag__get_document_section
|
||||||
|
- mcp__plugin_haiku-rag_haiku-rag__list_documents
|
||||||
|
- mcp__plugin_haiku-rag_haiku-rag__ask_question
|
||||||
|
- mcp__plugin_haiku-rag_haiku-rag__analyze
|
||||||
|
---
|
||||||
|
|
||||||
|
# Working with the knowledge base
|
||||||
|
|
||||||
|
Check the knowledge base before answering from memory whenever the question
|
||||||
|
could be about the user's documents. Say so when it has nothing relevant.
|
||||||
|
|
||||||
|
## Find
|
||||||
|
|
||||||
|
`search_documents` is the first call. Results come best first with the document
|
||||||
|
title, section headings and the matching passage. `filter` restricts which
|
||||||
|
documents are searched, `limit` how many results come back. If it misses,
|
||||||
|
rephrase once or narrow with a filter before concluding the material is not
|
||||||
|
there.
|
||||||
|
|
||||||
|
## Read
|
||||||
|
|
||||||
|
Every search result shows its `Document ID` (and `Collection` when there are
|
||||||
|
several); pass them to the read tools. `get_document` returns a document's
|
||||||
|
whole text in reading order. For a long one, `get_document_outline` gives the
|
||||||
|
heading tree with page numbers and `get_document_section` the text of one
|
||||||
|
section, subsections included.
|
||||||
|
|
||||||
|
## Answer or compute
|
||||||
|
|
||||||
|
`ask_question` runs the RAG agent on the server and returns an answer with
|
||||||
|
citations; use it when the user wants an answer rather than material.
|
||||||
|
`analyze` runs code in a sandbox over the documents; use it for counting,
|
||||||
|
aggregation, comparison across many documents or computation over tables. Both
|
||||||
|
cost a model call and are slower than a search.
|
||||||
|
|
||||||
|
## Explore
|
||||||
|
|
||||||
|
`list_documents` shows what is stored: titles, URIs and metadata. It is how you
|
||||||
|
learn what a filter can match.
|
||||||
|
|
||||||
|
## Filters
|
||||||
|
|
||||||
|
A SQL WHERE clause over the document columns `id`, `uri`, `title`,
|
||||||
|
`created_at`, `updated_at`, `metadata`. `metadata` is a JSON string, so match
|
||||||
|
it with LIKE: `metadata LIKE '%"author": "Smith"%'`. Also `uri LIKE '%.pdf'`,
|
||||||
|
`title = 'Q3 report'`.
|
||||||
|
|
||||||
|
## Results and citations
|
||||||
|
|
||||||
|
Rank is the signal; scores are not comparable across queries and are never
|
||||||
|
confidence. Cite the document title or URI, the section heading and page
|
||||||
|
numbers when present. When results carry `source`, the server covers several
|
||||||
|
collections: name it, and pass `sources` to search a subset.
|
||||||
19
docs/mcp.md
19
docs/mcp.md
|
|
@ -40,6 +40,25 @@ document. A name the server does not cover is an error.
|
||||||
`haiku-rag --db-name NAME mcp` serves one. See
|
`haiku-rag --db-name NAME mcp` serves one. See
|
||||||
[Multiple Databases](configuration/storage.md#multiple-databases).
|
[Multiple Databases](configuration/storage.md#multiple-databases).
|
||||||
|
|
||||||
|
## Claude Code
|
||||||
|
|
||||||
|
The repository ships a plugin that registers the server and a skill telling
|
||||||
|
Claude when and how to use it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude plugin marketplace add ggozad/haiku.rag
|
||||||
|
claude plugin install haiku-rag
|
||||||
|
```
|
||||||
|
|
||||||
|
The plugin runs `haiku-rag mcp --stdio`, so `haiku-rag` must be on the PATH
|
||||||
|
and the configuration decides the database. The skill pre-approves every tool
|
||||||
|
and is also invocable as `/haiku-rag`. To register the server without the
|
||||||
|
plugin:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude mcp add haiku-rag -- haiku-rag mcp --stdio
|
||||||
|
```
|
||||||
|
|
||||||
## Claude Desktop Integration
|
## Claude Desktop Integration
|
||||||
|
|
||||||
Add to your Claude Desktop configuration (`claude_desktop_config.json`):
|
Add to your Claude Desktop configuration (`claude_desktop_config.json`):
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
"""
|
"""
|
||||||
Version bumping script for haiku.rag workspace.
|
Version bumping script for haiku.rag workspace.
|
||||||
|
|
||||||
Updates version in all pyproject.toml files and CHANGELOG.md.
|
Updates version in all pyproject.toml files, the Claude Code plugin manifest
|
||||||
|
and CHANGELOG.md.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
@ -54,6 +55,19 @@ def update_example_dependencies(file_path: Path, new_version: str) -> None:
|
||||||
print(f"✓ Updated example dependencies in {file_path.relative_to(Path.cwd())}")
|
print(f"✓ Updated example dependencies in {file_path.relative_to(Path.cwd())}")
|
||||||
|
|
||||||
|
|
||||||
|
def update_plugin_version(file_path: Path, new_version: str) -> None:
|
||||||
|
"""Update the version in the Claude Code plugin manifest."""
|
||||||
|
content = file_path.read_text()
|
||||||
|
updated = re.sub(
|
||||||
|
r'^(\s*"version": )"[^"]+"',
|
||||||
|
rf'\1"{new_version}"',
|
||||||
|
content,
|
||||||
|
flags=re.MULTILINE,
|
||||||
|
)
|
||||||
|
file_path.write_text(updated)
|
||||||
|
print(f"✓ Updated {file_path.relative_to(Path.cwd())}")
|
||||||
|
|
||||||
|
|
||||||
def update_changelog(changelog_path: Path, new_version: str) -> None:
|
def update_changelog(changelog_path: Path, new_version: str) -> None:
|
||||||
"""Update CHANGELOG.md with new version."""
|
"""Update CHANGELOG.md with new version."""
|
||||||
content = changelog_path.read_text()
|
content = changelog_path.read_text()
|
||||||
|
|
@ -122,10 +136,13 @@ def main():
|
||||||
root / "app" / "backend" / "pyproject.toml",
|
root / "app" / "backend" / "pyproject.toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
plugin_file = root / "claude-plugin" / ".claude-plugin" / "plugin.json"
|
||||||
changelog_file = root / "CHANGELOG.md"
|
changelog_file = root / "CHANGELOG.md"
|
||||||
|
|
||||||
# Check all files exist
|
# Check all files exist
|
||||||
for file in pyproject_files + example_pyproject_files + [changelog_file]:
|
for file in (
|
||||||
|
pyproject_files + example_pyproject_files + [plugin_file, changelog_file]
|
||||||
|
):
|
||||||
if not file.exists():
|
if not file.exists():
|
||||||
print(f"Error: {file} not found")
|
print(f"Error: {file} not found")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
@ -155,6 +172,8 @@ def main():
|
||||||
for file in example_pyproject_files:
|
for file in example_pyproject_files:
|
||||||
update_example_dependencies(file, new_version)
|
update_example_dependencies(file, new_version)
|
||||||
|
|
||||||
|
update_plugin_version(plugin_file, new_version)
|
||||||
|
|
||||||
# Update CHANGELOG.md
|
# Update CHANGELOG.md
|
||||||
update_changelog(changelog_file, new_version)
|
update_changelog(changelog_file, new_version)
|
||||||
|
|
||||||
|
|
|
||||||
38
tests/test_bump_version.py
Normal file
38
tests/test_bump_version.py
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_spec = importlib.util.spec_from_file_location(
|
||||||
|
"bump_version", Path(__file__).resolve().parents[1] / "scripts" / "bump_version.py"
|
||||||
|
)
|
||||||
|
assert _spec is not None and _spec.loader is not None
|
||||||
|
bump_version = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(bump_version)
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_plugin_version_rewrites_only_the_version_field(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
manifest = tmp_path / "plugin.json"
|
||||||
|
manifest.write_text(
|
||||||
|
'{\n "name": "haiku-rag",\n "version": "0.1.0",\n "license": "MIT"\n}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
bump_version.update_plugin_version(manifest, "0.2.0")
|
||||||
|
|
||||||
|
assert json.loads(manifest.read_text()) == {
|
||||||
|
"name": "haiku-rag",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"license": "MIT",
|
||||||
|
}
|
||||||
|
assert manifest.read_text().endswith("}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_shipped_plugin_manifest_carries_the_package_version():
|
||||||
|
root = Path(__file__).resolve().parents[1]
|
||||||
|
plugin = json.loads(
|
||||||
|
(root / "claude-plugin" / ".claude-plugin" / "plugin.json").read_text()
|
||||||
|
)
|
||||||
|
|
||||||
|
assert plugin["version"] == bump_version.get_current_version(
|
||||||
|
root / "haiku_rag_slim" / "pyproject.toml"
|
||||||
|
)
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -1122,6 +1123,48 @@ class TestMCPErrorContract:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestClaudeCodePlugin:
|
||||||
|
"""The plugin under claude-plugin/ points at the server this module builds."""
|
||||||
|
|
||||||
|
root = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
def test_the_manifests_name_the_plugin_and_its_server(self):
|
||||||
|
import json
|
||||||
|
|
||||||
|
plugin = json.loads(
|
||||||
|
(self.root / "claude-plugin/.claude-plugin/plugin.json").read_text()
|
||||||
|
)
|
||||||
|
marketplace = json.loads(
|
||||||
|
(self.root / ".claude-plugin/marketplace.json").read_text()
|
||||||
|
)
|
||||||
|
servers = json.loads((self.root / "claude-plugin/.mcp.json").read_text())
|
||||||
|
|
||||||
|
assert plugin["name"] == "haiku-rag"
|
||||||
|
assert plugin["description"]
|
||||||
|
[entry] = marketplace["plugins"]
|
||||||
|
assert entry["name"] == plugin["name"]
|
||||||
|
assert entry["source"] == "./claude-plugin"
|
||||||
|
assert servers["mcpServers"]["haiku-rag"]["args"] == ["mcp", "--stdio"]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_the_skill_pre_approves_every_tool_the_server_registers(
|
||||||
|
self, mcp_db, multimodal_embedder
|
||||||
|
):
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
text = (self.root / "claude-plugin/skills/haiku-rag/SKILL.md").read_text()
|
||||||
|
_, frontmatter, _ = text.split("---", 2)
|
||||||
|
skill = yaml.safe_load(frontmatter)
|
||||||
|
prefix = "mcp__plugin_haiku-rag_haiku-rag__"
|
||||||
|
|
||||||
|
assert skill["name"] == "haiku-rag"
|
||||||
|
assert skill["description"]
|
||||||
|
assert all(tool.startswith(prefix) for tool in skill["allowed-tools"])
|
||||||
|
approved = {tool.removeprefix(prefix) for tool in skill["allowed-tools"]}
|
||||||
|
registered = {t.name for t in await create_mcp_server(mcp_db).list_tools()}
|
||||||
|
assert approved == registered
|
||||||
|
|
||||||
|
|
||||||
class TestMCPClientLifetime:
|
class TestMCPClientLifetime:
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_tool_calls_share_one_database_open(self, mcp_db, monkeypatch):
|
async def test_tool_calls_share_one_database_open(self, mcp_db, monkeypatch):
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue