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
|
||||
|
||||
- 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`
|
||||
unregistered. `create_mcp_server(agents=)`, `HaikuRAGApp.run_mcp(agents=)`.
|
||||
- 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
|
||||
|
||||
Use with AI assistants like Claude Desktop:
|
||||
Use with AI assistants like Claude Code and Claude Desktop:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```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
|
||||
[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
|
||||
|
||||
Add to your Claude Desktop configuration (`claude_desktop_config.json`):
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
"""
|
||||
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
|
||||
|
|
@ -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())}")
|
||||
|
||||
|
||||
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:
|
||||
"""Update CHANGELOG.md with new version."""
|
||||
content = changelog_path.read_text()
|
||||
|
|
@ -122,10 +136,13 @@ def main():
|
|||
root / "app" / "backend" / "pyproject.toml",
|
||||
]
|
||||
|
||||
plugin_file = root / "claude-plugin" / ".claude-plugin" / "plugin.json"
|
||||
changelog_file = root / "CHANGELOG.md"
|
||||
|
||||
# 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():
|
||||
print(f"Error: {file} not found")
|
||||
sys.exit(1)
|
||||
|
|
@ -155,6 +172,8 @@ def main():
|
|||
for file in example_pyproject_files:
|
||||
update_example_dependencies(file, new_version)
|
||||
|
||||
update_plugin_version(plugin_file, new_version)
|
||||
|
||||
# Update CHANGELOG.md
|
||||
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
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
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:
|
||||
@pytest.mark.asyncio
|
||||
async def test_tool_calls_share_one_database_open(self, mcp_db, monkeypatch):
|
||||
|
|
|
|||
Loading…
Reference in a new issue