feat(mcp): add describe_tools and annotate_tool functionality

* Implement DescribeTool for listing available MCP tools with annotations.
* Add UpsertToolAnnotation and GetToolAnnotations methods for managing tool notes.
* Create tool_annotations table for storing tool usage notes.
This commit is contained in:
Hein
2026-04-02 13:51:09 +02:00
parent 24532ef380
commit d0bfdbfbab
12 changed files with 393 additions and 19 deletions

View File

@@ -61,6 +61,32 @@ A Go MCP server for capturing and retrieving thoughts, memory, and project conte
| `remove_project_guardrail` | Unlink an agent guardrail from a project; pass `project` explicitly if your client does not preserve MCP sessions |
| `list_project_guardrails` | List all guardrails linked to a project; pass `project` explicitly if your client does not preserve MCP sessions |
| `get_version_info` | Return the server build version information, including version, tag name, commit, and build date |
| `describe_tools` | List all available MCP tools with names, descriptions, categories, and model-authored usage notes; call this at the start of a session to orient yourself |
| `annotate_tool` | Persist your own usage notes for a specific tool; notes are returned by `describe_tools` in future sessions |
## Self-Documenting Tools
AMCS includes a built-in tool directory that models can read and annotate.
**`describe_tools`** returns every registered tool with its name, description, category, and any model-written notes. Call it with no arguments to get the full list, or filter by category:
```json
{ "category": "thoughts" }
```
Available categories: `system`, `thoughts`, `projects`, `files`, `admin`, `household`, `maintenance`, `calendar`, `meals`, `crm`, `skills`, `chat`, `meta`.
**`annotate_tool`** lets a model write persistent usage notes against a tool name. Notes survive across sessions and are returned by `describe_tools`:
```json
{ "tool_name": "capture_thought", "notes": "Always pass project explicitly — session state is not reliable in this client." }
```
Pass an empty string to clear notes. The intended workflow is:
1. At the start of a session, call `describe_tools` to discover tools and read accumulated notes.
2. As you learn something non-obvious about a tool — a gotcha, a workflow pattern, a required field ordering — call `annotate_tool` to record it.
3. Future sessions receive the annotation automatically via `describe_tools`.
## MCP Error Contract

BIN
assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

View File

@@ -188,6 +188,7 @@ func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *st
CRM: tools.NewCRMTool(db),
Skills: tools.NewSkillsTool(db, activeProjects),
ChatHistory: tools.NewChatHistoryTool(db, activeProjects),
Describe: tools.NewDescribeTool(db, mcpserver.BuildToolCatalog()),
}
mcpHandler, err := mcpserver.New(cfg.MCP, logger, toolSet, activeProjects.Clear)
@@ -207,6 +208,7 @@ func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *st
}
mux.HandleFunc("/favicon.ico", serveFavicon)
mux.HandleFunc("/images/project.jpg", serveHomeImage)
mux.HandleFunc("/images/icon.png", serveIcon)
mux.HandleFunc("/llm", serveLLMInstructions)
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
@@ -342,3 +344,26 @@ func serveHomeImage(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(homeImage)
}
func serveIcon(w http.ResponseWriter, r *http.Request) {
if iconImage == nil {
http.NotFound(w, r)
return
}
if r.Method != http.MethodGet && r.Method != http.MethodHead {
w.Header().Set("Allow", "GET, HEAD")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "image/png")
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
w.WriteHeader(http.StatusOK)
if r.Method == http.MethodHead {
return
}
_, _ = w.Write(iconImage)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

View File

@@ -12,6 +12,7 @@ var (
faviconICO = mustReadStaticFile("favicon.ico")
homeImage = mustReadStaticFile("avelonmemorycrystal.jpg")
iconImage = tryReadStaticFile("icon.png")
)
func mustReadStaticFile(name string) []byte {
@@ -22,3 +23,11 @@ func mustReadStaticFile(name string) []byte {
return data
}
func tryReadStaticFile(name string) []byte {
data, err := fs.ReadFile(staticFiles, "static/"+name)
if err != nil {
return nil
}
return data
}

View File

@@ -36,6 +36,11 @@ type MCPConfig struct {
Version string `yaml:"version"`
Transport string `yaml:"transport"`
SessionTimeout time.Duration `yaml:"session_timeout"`
// PublicURL is the externally reachable base URL of this server (e.g. https://amcs.example.com).
// When set, it is used to build absolute icon URLs in the MCP server identity.
PublicURL string `yaml:"public_url"`
// Instructions is set at startup from the embedded memory.md and sent to MCP clients on initialise.
Instructions string `yaml:"-"`
}
type AuthConfig struct {

View File

@@ -117,6 +117,7 @@ func defaultConfig() Config {
func applyEnvOverrides(cfg *Config) {
overrideString(&cfg.Database.URL, "AMCS_DATABASE_URL")
overrideString(&cfg.MCP.PublicURL, "AMCS_PUBLIC_URL")
overrideString(&cfg.AI.LiteLLM.BaseURL, "AMCS_LITELLM_BASE_URL")
overrideString(&cfg.AI.LiteLLM.APIKey, "AMCS_LITELLM_API_KEY")
overrideString(&cfg.AI.Ollama.BaseURL, "AMCS_OLLAMA_BASE_URL")

View File

@@ -3,11 +3,18 @@ package mcpserver
import (
"log/slog"
"net/http"
"strings"
"github.com/modelcontextprotocol/go-sdk/mcp"
"git.warky.dev/wdevs/amcs/internal/config"
"git.warky.dev/wdevs/amcs/internal/tools"
amcsllm "git.warky.dev/wdevs/amcs/llm"
)
const (
serverTitle = "Avalon Memory Crystal Server"
serverWebsiteURL = "https://git.warky.dev/wdevs/amcs"
)
type ToolSet struct {
@@ -36,13 +43,24 @@ type ToolSet struct {
CRM *tools.CRMTool
Skills *tools.SkillsTool
ChatHistory *tools.ChatHistoryTool
Describe *tools.DescribeTool
}
func New(cfg config.MCPConfig, logger *slog.Logger, toolSet ToolSet, onSessionClosed func(string)) (http.Handler, error) {
instructions := cfg.Instructions
if instructions == "" {
instructions = string(amcsllm.MemoryInstructions)
}
server := mcp.NewServer(&mcp.Implementation{
Name: cfg.ServerName,
Version: cfg.Version,
}, nil)
Name: cfg.ServerName,
Title: serverTitle,
Version: cfg.Version,
WebsiteURL: serverWebsiteURL,
Icons: buildServerIcons(cfg.PublicURL),
}, &mcp.ServerOptions{
Instructions: instructions,
})
for _, register := range []func(*mcp.Server, *slog.Logger, ToolSet) error{
registerSystemTools,
@@ -56,6 +74,7 @@ func New(cfg config.MCPConfig, logger *slog.Logger, toolSet ToolSet, onSessionCl
registerCRMTools,
registerSkillTools,
registerChatHistoryTools,
registerDescribeTools,
} {
if err := register(server, logger, toolSet); err != nil {
return nil, err
@@ -75,6 +94,18 @@ func New(cfg config.MCPConfig, logger *slog.Logger, toolSet ToolSet, onSessionCl
}, opts), nil
}
// buildServerIcons returns icon definitions referencing the server's own /images/icon.png endpoint.
// Returns nil when publicURL is empty so the icons field is omitted from the MCP identity.
func buildServerIcons(publicURL string) []mcp.Icon {
if strings.TrimSpace(publicURL) == "" {
return nil
}
base := strings.TrimRight(publicURL, "/")
return []mcp.Icon{
{Source: base + "/images/icon.png", MIMEType: "image/png"},
}
}
func registerSystemTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
if err := addTool(server, logger, &mcp.Tool{
Name: "get_version_info",
@@ -88,13 +119,13 @@ func registerSystemTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSe
func registerThoughtTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
if err := addTool(server, logger, &mcp.Tool{
Name: "capture_thought",
Description: "Store a thought with generated embeddings and extracted metadata.",
Description: "Store a thought with generated embeddings and extracted metadata. The thought is saved immediately even if metadata extraction times out; pending thoughts are retried in the background.",
}, toolSet.Capture.Handle); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "search_thoughts",
Description: "Search stored thoughts by semantic similarity.",
Description: "Search stored thoughts by semantic similarity. Falls back to Postgres full-text search automatically when no embeddings exist for the active model.",
}, toolSet.Search.Handle); err != nil {
return err
}
@@ -136,13 +167,13 @@ func registerThoughtTools(server *mcp.Server, logger *slog.Logger, toolSet ToolS
}
if err := addTool(server, logger, &mcp.Tool{
Name: "summarize_thoughts",
Description: "Summarize a filtered or searched set of thoughts.",
Description: "Produce an LLM prose summary of a filtered or searched set of thoughts.",
}, toolSet.Summarize.Handle); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "recall_context",
Description: "Recall semantically relevant and recent context.",
Description: "Recall semantically relevant and recent context for prompt injection. Combines vector similarity with recency. Falls back to full-text search when no embeddings exist.",
}, toolSet.Recall.Handle); err != nil {
return err
}
@@ -154,7 +185,7 @@ func registerThoughtTools(server *mcp.Server, logger *slog.Logger, toolSet ToolS
}
if err := addTool(server, logger, &mcp.Tool{
Name: "related_thoughts",
Description: "Retrieve explicit links and semantic neighbors for a thought.",
Description: "Retrieve explicit links and semantic neighbours for a thought. Falls back to full-text search when no embeddings exist.",
}, toolSet.Links.Related); err != nil {
return err
}
@@ -176,19 +207,19 @@ func registerProjectTools(server *mcp.Server, logger *slog.Logger, toolSet ToolS
}
if err := addTool(server, logger, &mcp.Tool{
Name: "set_active_project",
Description: "Set the active project for the current MCP session. Requires a stateful MCP client that reuses the same session across calls.",
Description: "Set the active project for the current MCP session. Requires a stateful MCP client that reuses the same session across calls. If your client does not preserve sessions, pass project explicitly to each tool instead.",
}, toolSet.Projects.SetActive); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "get_active_project",
Description: "Return the active project for the current MCP session. If your client does not preserve MCP sessions, pass project explicitly to project-scoped tools instead.",
Description: "Return the active project for the current MCP session. If your client does not preserve MCP sessions, pass project explicitly to project-scoped tools instead of relying on this.",
}, toolSet.Projects.GetActive); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "get_project_context",
Description: "Get recent and semantic context for a project. Uses the explicit project when provided, otherwise the active MCP session project.",
Description: "Get recent and semantic context for a project. Uses the explicit project when provided, otherwise the active MCP session project. Falls back to full-text search when no embeddings exist.",
}, toolSet.Context.Handle); err != nil {
return err
}
@@ -204,19 +235,19 @@ func registerFileTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet)
if err := addTool(server, logger, &mcp.Tool{
Name: "upload_file",
Description: "Stage a file and get an amcs://files/{id} resource URI. Provide content_path (absolute server-side path, no size limit) or content_base64 (≤10 MB). Optionally link immediately with thought_id/project, or omit them and pass the returned URI to save_file later.",
Description: "Stage a file and get an amcs://files/{id} resource URI. Use content_path (absolute server-side path, no size limit) for large or binary files, or content_base64 (≤10 MB) for small files. Pass thought_id/project to link immediately, or omit and pass the URI to save_file later.",
}, toolSet.Files.Upload); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "save_file",
Description: "Store a file and optionally link it to a thought. Supply either content_base64 (≤10 MB) or content_uri (amcs://files/{id} from a prior upload_file or POST /files call). For files larger than 10 MB, use upload_file with content_path first.",
Description: "Store a file and optionally link it to a thought. Use content_base64 (≤10 MB) for small files, or content_uri (amcs://files/{id} from a prior upload_file) for previously staged files. For files larger than 10 MB, use upload_file with content_path first. If the goal is to retain the artifact, store the file directly instead of reading or summarising it first.",
}, toolSet.Files.Save); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "load_file",
Description: "Load a previously stored file by id and return its metadata and base64 content.",
Description: "Load a stored file by id. Returns metadata, base64 content, and an embedded MCP binary resource at amcs://files/{id}. Prefer the embedded resource when your client supports it. The id field accepts a bare UUID or full amcs://files/{id} URI.",
}, toolSet.Files.Load); err != nil {
return err
}
@@ -232,7 +263,7 @@ func registerFileTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet)
func registerMaintenanceTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
if err := addTool(server, logger, &mcp.Tool{
Name: "backfill_embeddings",
Description: "Generate missing embeddings for stored thoughts using the active embedding model.",
Description: "Generate missing embeddings for stored thoughts using the active embedding model. Run this after switching embedding models or importing thoughts that have no vectors.",
}, toolSet.Backfill.Handle); err != nil {
return err
}
@@ -492,7 +523,7 @@ func registerSkillTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet
}
if err := addTool(server, logger, &mcp.Tool{
Name: "list_project_skills",
Description: "List all skills linked to a project. Call this at the start of a project session to load existing agent behaviour instructions before generating new ones. Pass project explicitly when your client does not preserve MCP sessions.",
Description: "List all skills linked to a project. Call this at the start of every project session to load agent behaviour instructions before generating new ones. Only create new skills if none are returned. Pass project explicitly when your client does not preserve MCP sessions.",
}, toolSet.Skills.ListProjectSkills); err != nil {
return err
}
@@ -510,7 +541,7 @@ func registerSkillTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet
}
if err := addTool(server, logger, &mcp.Tool{
Name: "list_project_guardrails",
Description: "List all guardrails linked to a project. Call this at the start of a project session to load existing agent constraints before generating new ones. Pass project explicitly when your client does not preserve MCP sessions.",
Description: "List all guardrails linked to a project. Call this at the start of every project session to load agent constraints before generating new ones. Only create new guardrails if none are returned. Pass project explicitly when your client does not preserve MCP sessions.",
}, toolSet.Skills.ListProjectGuardrails); err != nil {
return err
}
@@ -544,3 +575,123 @@ func registerChatHistoryTools(server *mcp.Server, logger *slog.Logger, toolSet T
}
return nil
}
func registerDescribeTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
if err := addTool(server, logger, &mcp.Tool{
Name: "describe_tools",
Description: "Call this first in every session. Returns all available MCP tools with names, descriptions, categories, and your accumulated usage notes. Filter by category to narrow results. Available categories: system, thoughts, projects, files, admin, household, maintenance, calendar, meals, crm, skills, chat, meta.",
}, toolSet.Describe.Describe); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "annotate_tool",
Description: "Persist usage notes, gotchas, or workflow patterns for a specific tool. Notes survive across sessions and are returned by describe_tools. Call this whenever you discover something non-obvious about a tool's behaviour. Pass an empty string to clear notes.",
}, toolSet.Describe.Annotate); err != nil {
return err
}
return nil
}
// BuildToolCatalog returns the static catalog of all registered MCP tools.
// Pass this to tools.NewDescribeTool when assembling the ToolSet.
func BuildToolCatalog() []tools.ToolEntry {
return []tools.ToolEntry{
// system
{Name: "get_version_info", Description: "Return the server build version information, including version, tag name, commit, and build date.", Category: "system"},
// thoughts
{Name: "capture_thought", Description: "Store a thought with generated embeddings and extracted metadata. The thought is saved immediately even if metadata extraction times out; pending thoughts are retried in the background.", Category: "thoughts"},
{Name: "search_thoughts", Description: "Search stored thoughts by semantic similarity. Falls back to Postgres full-text search automatically when no embeddings exist for the active model.", Category: "thoughts"},
{Name: "list_thoughts", Description: "List recent thoughts with optional metadata filters.", Category: "thoughts"},
{Name: "thought_stats", Description: "Get counts and top metadata buckets across stored thoughts.", Category: "thoughts"},
{Name: "get_thought", Description: "Retrieve a full thought by id.", Category: "thoughts"},
{Name: "update_thought", Description: "Update thought content or merge metadata.", Category: "thoughts"},
{Name: "delete_thought", Description: "Hard-delete a thought by id.", Category: "thoughts"},
{Name: "archive_thought", Description: "Archive a thought so it is hidden from default search and listing.", Category: "thoughts"},
{Name: "summarize_thoughts", Description: "Produce an LLM prose summary of a filtered or searched set of thoughts.", Category: "thoughts"},
{Name: "recall_context", Description: "Recall semantically relevant and recent context for prompt injection. Combines vector similarity with recency. Falls back to full-text search when no embeddings exist.", Category: "thoughts"},
{Name: "link_thoughts", Description: "Create a typed relationship between two thoughts.", Category: "thoughts"},
{Name: "related_thoughts", Description: "Retrieve explicit links and semantic neighbours for a thought. Falls back to full-text search when no embeddings exist.", Category: "thoughts"},
// projects
{Name: "create_project", Description: "Create a named project container for thoughts.", Category: "projects"},
{Name: "list_projects", Description: "List projects and their current thought counts.", Category: "projects"},
{Name: "set_active_project", Description: "Set the active project for the current MCP session. Requires a stateful MCP client that reuses the same session across calls. If your client does not preserve sessions, pass project explicitly to each tool instead.", Category: "projects"},
{Name: "get_active_project", Description: "Return the active project for the current MCP session. If your client does not preserve MCP sessions, pass project explicitly to project-scoped tools instead of relying on this.", Category: "projects"},
{Name: "get_project_context", Description: "Get recent and semantic context for a project. Uses the explicit project when provided, otherwise the active MCP session project. Falls back to full-text search when no embeddings exist.", Category: "projects"},
// files
{Name: "upload_file", Description: "Stage a file and get an amcs://files/{id} resource URI. Use content_path (absolute server-side path, no size limit) for large or binary files, or content_base64 (≤10 MB) for small files. Pass thought_id/project to link immediately, or omit and pass the URI to save_file later.", Category: "files"},
{Name: "save_file", Description: "Store a file and optionally link it to a thought. Use content_base64 (≤10 MB) for small files, or content_uri (amcs://files/{id} from a prior upload_file) for previously staged files. For files larger than 10 MB, use upload_file with content_path first. If the goal is to retain the artifact, store the file directly instead of reading or summarising it first.", Category: "files"},
{Name: "load_file", Description: "Load a stored file by id. Returns metadata, base64 content, and an embedded MCP binary resource at amcs://files/{id}. Prefer the embedded resource when your client supports it. The id field accepts a bare UUID or full amcs://files/{id} URI.", Category: "files"},
{Name: "list_files", Description: "List stored files, optionally filtered by thought, project, or kind.", Category: "files"},
// admin
{Name: "backfill_embeddings", Description: "Generate missing embeddings for stored thoughts using the active embedding model. Run this after switching embedding models or importing thoughts that have no vectors.", Category: "admin"},
{Name: "reparse_thought_metadata", Description: "Re-extract and normalize metadata for stored thoughts from their content.", Category: "admin"},
{Name: "retry_failed_metadata", Description: "Retry metadata extraction for thoughts still marked pending or failed.", Category: "admin"},
// household
{Name: "add_household_item", Description: "Store a household fact (paint color, appliance details, measurement, document, etc.).", Category: "household"},
{Name: "search_household_items", Description: "Search household items by name, category, or location.", Category: "household"},
{Name: "get_household_item", Description: "Retrieve a household item by id.", Category: "household"},
{Name: "add_vendor", Description: "Add a service provider (plumber, electrician, landscaper, etc.).", Category: "household"},
{Name: "list_vendors", Description: "List household service vendors, optionally filtered by service type.", Category: "household"},
// maintenance
{Name: "add_maintenance_task", Description: "Create a recurring or one-time home maintenance task.", Category: "maintenance"},
{Name: "log_maintenance", Description: "Log completed maintenance work; automatically updates the task's next due date.", Category: "maintenance"},
{Name: "get_upcoming_maintenance", Description: "List maintenance tasks due within the next N days.", Category: "maintenance"},
{Name: "search_maintenance_history", Description: "Search the maintenance log by task name, category, or date range.", Category: "maintenance"},
// calendar
{Name: "add_family_member", Description: "Add a family member to the household.", Category: "calendar"},
{Name: "list_family_members", Description: "List all family members.", Category: "calendar"},
{Name: "add_activity", Description: "Schedule a one-time or recurring family activity.", Category: "calendar"},
{Name: "get_week_schedule", Description: "Get all activities scheduled for a given week.", Category: "calendar"},
{Name: "search_activities", Description: "Search activities by title, type, or family member.", Category: "calendar"},
{Name: "add_important_date", Description: "Track a birthday, anniversary, deadline, or other important date.", Category: "calendar"},
{Name: "get_upcoming_dates", Description: "Get important dates coming up in the next N days.", Category: "calendar"},
// meals
{Name: "add_recipe", Description: "Save a recipe with ingredients and instructions.", Category: "meals"},
{Name: "search_recipes", Description: "Search recipes by name, cuisine, tags, or ingredient.", Category: "meals"},
{Name: "update_recipe", Description: "Update an existing recipe.", Category: "meals"},
{Name: "create_meal_plan", Description: "Set the meal plan for a week; replaces any existing plan for that week.", Category: "meals"},
{Name: "get_meal_plan", Description: "Get the meal plan for a given week.", Category: "meals"},
{Name: "generate_shopping_list", Description: "Auto-generate a shopping list from the meal plan for a given week.", Category: "meals"},
// crm
{Name: "add_professional_contact", Description: "Add a professional contact to the CRM.", Category: "crm"},
{Name: "search_contacts", Description: "Search professional contacts by name, company, title, notes, or tags.", Category: "crm"},
{Name: "log_interaction", Description: "Log an interaction with a professional contact.", Category: "crm"},
{Name: "get_contact_history", Description: "Get full history (interactions and opportunities) for a contact.", Category: "crm"},
{Name: "create_opportunity", Description: "Create a deal, project, or opportunity linked to a contact.", Category: "crm"},
{Name: "get_follow_ups_due", Description: "List contacts with a follow-up date due within the next N days.", Category: "crm"},
{Name: "link_thought_to_contact", Description: "Append a stored thought to a contact's notes.", Category: "crm"},
// skills
{Name: "add_skill", Description: "Store a reusable agent skill (behavioural instruction or capability prompt).", Category: "skills"},
{Name: "remove_skill", Description: "Delete an agent skill by id.", Category: "skills"},
{Name: "list_skills", Description: "List all agent skills, optionally filtered by tag.", Category: "skills"},
{Name: "add_guardrail", Description: "Store a reusable agent guardrail (constraint or safety rule).", Category: "skills"},
{Name: "remove_guardrail", Description: "Delete an agent guardrail by id.", Category: "skills"},
{Name: "list_guardrails", Description: "List all agent guardrails, optionally filtered by tag or severity.", Category: "skills"},
{Name: "add_project_skill", Description: "Link an agent skill to a project. Pass project explicitly when your client does not preserve MCP sessions.", Category: "skills"},
{Name: "remove_project_skill", Description: "Unlink an agent skill from a project. Pass project explicitly when your client does not preserve MCP sessions.", Category: "skills"},
{Name: "list_project_skills", Description: "List all skills linked to a project. Call this at the start of every project session to load agent behaviour instructions before generating new ones. Only create new skills if none are returned. Pass project explicitly when your client does not preserve MCP sessions.", Category: "skills"},
{Name: "add_project_guardrail", Description: "Link an agent guardrail to a project. Pass project explicitly when your client does not preserve MCP sessions.", Category: "skills"},
{Name: "remove_project_guardrail", Description: "Unlink an agent guardrail from a project. Pass project explicitly when your client does not preserve MCP sessions.", Category: "skills"},
{Name: "list_project_guardrails", Description: "List all guardrails linked to a project. Call this at the start of every project session to load agent constraints before generating new ones. Only create new guardrails if none are returned. Pass project explicitly when your client does not preserve MCP sessions.", Category: "skills"},
// chat
{Name: "save_chat_history", Description: "Save a chat session's message history for later retrieval. Stores messages with optional title, summary, channel, agent, and project metadata.", Category: "chat"},
{Name: "get_chat_history", Description: "Retrieve a saved chat history by its UUID or session_id. Returns the full message list.", Category: "chat"},
{Name: "list_chat_histories", Description: "List saved chat histories with optional filters: project, channel, agent_id, session_id, or recent days.", Category: "chat"},
{Name: "delete_chat_history", Description: "Permanently delete a saved chat history by id.", Category: "chat"},
// meta
{Name: "describe_tools", Description: "Call this first in every session. Returns all available MCP tools with names, descriptions, categories, and your accumulated usage notes. Filter by category to narrow results. Available categories: system, thoughts, projects, files, admin, household, maintenance, calendar, meals, crm, skills, chat, meta.", Category: "meta"},
{Name: "annotate_tool", Description: "Persist usage notes, gotchas, or workflow patterns for a specific tool. Notes survive across sessions and are returned by describe_tools. Call this whenever you discover something non-obvious about a tool's behaviour. Pass an empty string to clear notes.", Category: "meta"},
}
}

View File

@@ -0,0 +1,38 @@
package store
import (
"context"
"fmt"
)
func (db *DB) UpsertToolAnnotation(ctx context.Context, toolName, notes string) error {
_, err := db.pool.Exec(ctx, `
insert into tool_annotations (tool_name, notes)
values ($1, $2)
on conflict (tool_name) do update
set notes = excluded.notes,
updated_at = now()
`, toolName, notes)
if err != nil {
return fmt.Errorf("upsert tool annotation: %w", err)
}
return nil
}
func (db *DB) GetToolAnnotations(ctx context.Context) (map[string]string, error) {
rows, err := db.pool.Query(ctx, `select tool_name, notes from tool_annotations`)
if err != nil {
return nil, fmt.Errorf("get tool annotations: %w", err)
}
defer rows.Close()
annotations := make(map[string]string)
for rows.Next() {
var toolName, notes string
if err := rows.Scan(&toolName, &notes); err != nil {
return nil, fmt.Errorf("scan tool annotation: %w", err)
}
annotations[toolName] = notes
}
return annotations, rows.Err()
}

View File

@@ -0,0 +1,89 @@
package tools
import (
"context"
"strings"
"github.com/modelcontextprotocol/go-sdk/mcp"
"git.warky.dev/wdevs/amcs/internal/store"
)
// ToolEntry describes a single registered MCP tool.
type ToolEntry struct {
Name string
Description string
Category string
}
// DescribeTool implements the describe_tools and annotate_tool MCP tools.
type DescribeTool struct {
store *store.DB
catalog []ToolEntry
}
func NewDescribeTool(db *store.DB, catalog []ToolEntry) *DescribeTool {
return &DescribeTool{store: db, catalog: catalog}
}
// describe_tools
type DescribeToolsInput struct {
Category string `json:"category,omitempty" jsonschema:"filter results to a single category (e.g. thoughts, projects, files, skills, chat, meta)"`
}
type AnnotatedToolEntry struct {
Name string `json:"name"`
Description string `json:"description"`
Category string `json:"category"`
Notes string `json:"notes,omitempty"`
}
type DescribeToolsOutput struct {
Tools []AnnotatedToolEntry `json:"tools"`
}
func (t *DescribeTool) Describe(ctx context.Context, _ *mcp.CallToolRequest, in DescribeToolsInput) (*mcp.CallToolResult, DescribeToolsOutput, error) {
annotations, err := t.store.GetToolAnnotations(ctx)
if err != nil {
return nil, DescribeToolsOutput{}, err
}
cat := strings.TrimSpace(strings.ToLower(in.Category))
entries := make([]AnnotatedToolEntry, 0, len(t.catalog))
for _, e := range t.catalog {
if cat != "" && e.Category != cat {
continue
}
entries = append(entries, AnnotatedToolEntry{
Name: e.Name,
Description: e.Description,
Category: e.Category,
Notes: annotations[e.Name],
})
}
return nil, DescribeToolsOutput{Tools: entries}, nil
}
// annotate_tool
type AnnotateToolInput struct {
ToolName string `json:"tool_name" jsonschema:"the exact name of the tool to annotate"`
Notes string `json:"notes" jsonschema:"your usage notes, reminders, or gotchas for this tool; pass empty string to clear"`
}
type AnnotateToolOutput struct {
ToolName string `json:"tool_name"`
}
func (t *DescribeTool) Annotate(ctx context.Context, _ *mcp.CallToolRequest, in AnnotateToolInput) (*mcp.CallToolResult, AnnotateToolOutput, error) {
if strings.TrimSpace(in.ToolName) == "" {
return nil, AnnotateToolOutput{}, errRequiredField("tool_name")
}
if err := t.store.UpsertToolAnnotation(ctx, in.ToolName, in.Notes); err != nil {
return nil, AnnotateToolOutput{}, err
}
return nil, AnnotateToolOutput{ToolName: in.ToolName}, nil
}

View File

@@ -18,9 +18,15 @@ Use AMCS as memory with two scopes:
6. If no strong project match exists, you may use global notebook memory with no project.
7. If multiple projects plausibly match, ask the user before reading or writing project memory.
## Session Startup
At the very start of any session with AMCS:
1. Call `describe_tools` to get the full list of available tools with their categories and any notes you have previously annotated. Read the notes before using a tool — they contain accumulated gotchas, workflow patterns, and field-ordering requirements you have recorded from prior sessions.
## Project Session Startup
At the start of every project session, after setting the active project:
After setting the active project:
1. Call `list_project_skills` to load any saved agent behaviour instructions for the project.
2. Call `list_project_guardrails` to load any saved agent constraints for the project.
@@ -59,6 +65,16 @@ At the start of every project session, after setting the active project:
- Do not base64-encode a file to pass it to `save_file` if an `amcs://files/{id}` URI is already available from a prior `upload_file` or HTTP upload.
- When saving, choose the narrowest correct scope: project if project-specific, global if not.
## Tool Annotations
As you learn non-obvious behaviours, gotchas, or workflow patterns for individual tools, persist them with `annotate_tool`:
```json
{ "tool_name": "capture_thought", "notes": "Always pass project explicitly — session state is unreliable in this client." }
```
Notes are returned by `describe_tools` in future sessions. Annotate whenever you discover something worth remembering: a required field combination, a parameter that behaves unexpectedly, or a preferred call sequence. Pass an empty string to clear a note.
## Skills and Guardrails
- **Skills** are reusable agent behaviour instructions (e.g. output formatting rules, reasoning strategies, workflow conventions).
@@ -69,4 +85,4 @@ At the start of every project session, after setting the active project:
## Short Operational Form
Use AMCS memory in project scope when the current work matches a known project. If no clear project matches, global notebook memory is allowed for non-project-specific information. At the start of every project session call `list_project_skills` and `list_project_guardrails` and apply what is returned; only create new skills or guardrails if none exist. If your MCP client does not preserve sessions across calls, pass `project` explicitly instead of relying on `set_active_project`. Store durable notes with `capture_thought`. For binary files or files larger than 10 MB, call `upload_file` with `content_path` to stage the file and get an `amcs://files/{id}` URI, then pass that URI to `save_file` as `content_uri` to link it to a thought. For small files, use `save_file` or `upload_file` with `content_base64` directly. Browse stored files with `list_files`, and load them with `load_file` only when their contents are needed. Stored files can also be read as raw binary via MCP resources at `amcs://files/{id}`. Never store project-specific memory globally when a matching project exists, and never store memory in the wrong project. If project matching is ambiguous, ask the user.
At the start of every session, call `describe_tools` to read the full tool list and any accumulated usage notes. Use AMCS memory in project scope when the current work matches a known project; if no clear project matches, global notebook memory is allowed for non-project-specific information. At the start of every project session call `list_project_skills` and `list_project_guardrails` and apply what is returned; only create new skills or guardrails if none exist. If your MCP client does not preserve sessions across calls, pass `project` explicitly instead of relying on `set_active_project`. Store durable notes with `capture_thought`. For binary files or files larger than 10 MB, call `upload_file` with `content_path` to stage the file and get an `amcs://files/{id}` URI, then pass that URI to `save_file` as `content_uri` to link it to a thought. For small files, use `save_file` or `upload_file` with `content_base64` directly. Browse stored files with `list_files`, and load them with `load_file` only when their contents are needed. Stored files can also be read as raw binary via MCP resources at `amcs://files/{id}`. Never store project-specific memory globally when a matching project exists, and never store memory in the wrong project. If project matching is ambiguous, ask the user. Whenever you discover a non-obvious tool behaviour, gotcha, or workflow pattern, record it with `annotate_tool` so future sessions benefit.

View File

@@ -0,0 +1,14 @@
-- Migration: 019_tool_annotations
-- Adds a table for model-authored usage notes per tool.
create table if not exists tool_annotations (
id bigserial primary key,
tool_name text not null,
notes text not null default '',
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint tool_annotations_tool_name_unique unique (tool_name)
);
grant all on table public.tool_annotations to amcs;
grant usage, select on sequence tool_annotations_id_seq to amcs;