Files
amcs/internal/mcpserver/server.go
Hein (Warky) e285a03639
Some checks failed
CI / build-and-test (push) Failing after -31m18s
Add schema for agent personas, parts, traits, and character arcs
- Created tables for agent_personas, agent_parts, agent_traits, and character_arcs.
- Established relationships between personas, parts, skills, guardrails, and traits.
- Added arc stages and their corresponding parts, along with a persona_arc table to track current stages.
- Implemented cascading delete rules for referential integrity.
2026-05-05 09:43:14 +02:00

819 lines
42 KiB
Go

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 {
Version *tools.VersionTool
Capture *tools.CaptureTool
Search *tools.SearchTool
List *tools.ListTool
Stats *tools.StatsTool
Get *tools.GetTool
Update *tools.UpdateTool
Delete *tools.DeleteTool
Archive *tools.ArchiveTool
Projects *tools.ProjectsTool
Context *tools.ContextTool
Recall *tools.RecallTool
Summarize *tools.SummarizeTool
Links *tools.LinksTool
Files *tools.FilesTool
Backfill *tools.BackfillTool
Reparse *tools.ReparseMetadataTool
RetryMetadata *tools.RetryEnrichmentTool
//Maintenance *tools.MaintenanceTool
Skills *tools.SkillsTool
Personas *tools.AgentPersonasTool
ChatHistory *tools.ChatHistoryTool
Describe *tools.DescribeTool
Learnings *tools.LearningsTool
Plans *tools.PlansTool
}
// Handlers groups the HTTP handlers produced for an MCP server instance.
type Handlers struct {
// StreamableHTTP is the primary MCP handler (always non-nil).
StreamableHTTP http.Handler
// SSE is the SSE transport handler; nil when SSEPath is empty.
// SSE is the de facto transport for MCP over the internet and is required by most hosted MCP clients.
SSE http.Handler
}
// New builds the StreamableHTTP MCP handler. It is a convenience wrapper
// around NewHandlers for callers that only need the primary transport.
func New(cfg config.MCPConfig, logger *slog.Logger, toolSet ToolSet, onSessionClosed func(string)) (http.Handler, error) {
h, err := NewHandlers(cfg, logger, toolSet, onSessionClosed)
if err != nil {
return nil, err
}
return h.StreamableHTTP, nil
}
// NewHandlers builds MCP HTTP handlers for both transports.
// SSE is nil when cfg.SSEPath is empty.
func NewHandlers(cfg config.MCPConfig, logger *slog.Logger, toolSet ToolSet, onSessionClosed func(string)) (Handlers, error) {
instructions := cfg.Instructions
if instructions == "" {
instructions = string(amcsllm.MemoryInstructions)
}
server := mcp.NewServer(&mcp.Implementation{
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,
registerThoughtTools,
registerProjectTools,
registerLearningTools,
registerPlanTools,
registerFileTools,
registerMaintenanceTools,
registerSkillTools,
registerPersonaTools,
registerChatHistoryTools,
registerDescribeTools,
} {
if err := register(server, logger, toolSet); err != nil {
return Handlers{}, err
}
}
opts := &mcp.StreamableHTTPOptions{
JSONResponse: true,
SessionTimeout: cfg.SessionTimeout,
}
if onSessionClosed != nil {
opts.EventStore = newCleanupEventStore(mcp.NewMemoryEventStore(nil), onSessionClosed)
}
h := Handlers{
StreamableHTTP: mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server {
return server
}, opts),
}
if strings.TrimSpace(cfg.SSEPath) != "" {
h.SSE = mcp.NewSSEHandler(func(*http.Request) *mcp.Server {
return server
}, nil)
}
return h, 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",
Description: "Build version, commit, and date.",
}, toolSet.Version.GetInfo); err != nil {
return err
}
return nil
}
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; embeddings and metadata extracted async.",
}, toolSet.Capture.Handle); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "search_thoughts",
Description: "Semantic search; falls back to full-text if no embeddings.",
}, toolSet.Search.Handle); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "list_thoughts",
Description: "List recent thoughts with optional metadata filters.",
}, toolSet.List.Handle); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "thought_stats",
Description: "Counts and top metadata buckets for stored thoughts.",
}, toolSet.Stats.Handle); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "get_thought",
Description: "Retrieve a full thought by id.",
}, toolSet.Get.Handle); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "update_thought",
Description: "Update thought content or merge metadata.",
}, toolSet.Update.Handle); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "delete_thought",
Description: "Hard-delete a thought by id.",
}, toolSet.Delete.Handle); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "archive_thought",
Description: "Hide a thought from default search and listing.",
}, toolSet.Archive.Handle); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "summarize_thoughts",
Description: "LLM summary of a filtered set of thoughts.",
}, toolSet.Summarize.Handle); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "recall_context",
Description: "Semantic + recency context for prompt injection; falls back to full-text.",
}, toolSet.Recall.Handle); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "link_thoughts",
Description: "Create a typed relationship between two thoughts.",
}, toolSet.Links.Link); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "related_thoughts",
Description: "Explicit links and semantic neighbours; falls back to full-text.",
}, toolSet.Links.Related); err != nil {
return err
}
return nil
}
func registerProjectTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
if err := addTool(server, logger, &mcp.Tool{
Name: "create_project",
Description: "Create a named project container for thoughts.",
}, toolSet.Projects.Create); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "list_projects",
Description: "List projects and their current thought counts.",
}, toolSet.Projects.List); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "set_active_project",
Description: "Set session's active project. Pass project per call if client is stateless.",
}, toolSet.Projects.SetActive); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "get_active_project",
Description: "Return session's active project. Pass project per call if client is stateless.",
}, toolSet.Projects.GetActive); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "get_project_context",
Description: "Recent and semantic context for a project; falls back to full-text.",
}, toolSet.Context.Handle); err != nil {
return err
}
return nil
}
func registerLearningTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
if err := addTool(server, logger, &mcp.Tool{
Name: "add_learning",
Description: "Create a curated learning record distinct from raw thoughts.",
}, toolSet.Learnings.Add); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "get_learning",
Description: "Retrieve a structured learning by id.",
}, toolSet.Learnings.Get); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "list_learnings",
Description: "List structured learnings with optional project, status, priority, tag, and text filters.",
}, toolSet.Learnings.List); err != nil {
return err
}
return nil
}
func registerPlanTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
if err := addTool(server, logger, &mcp.Tool{
Name: "create_plan",
Description: "Create a structured plan linked to a project.",
}, toolSet.Plans.Create); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "get_plan",
Description: "Retrieve a plan with its dependencies, related plans, skills, and guardrails.",
}, toolSet.Plans.Get); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "update_plan",
Description: "Update plan fields; only provided fields are changed.",
}, toolSet.Plans.Update); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "delete_plan",
Description: "Hard-delete a plan by id.",
}, toolSet.Plans.Delete); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "list_plans",
Description: "List plans with optional project, status, priority, owner, tag, and text filters.",
}, toolSet.Plans.List); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "add_plan_dependency",
Description: "Mark plan_id as depending on depends_on_plan_id (must complete first).",
}, toolSet.Plans.AddDependency); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "remove_plan_dependency",
Description: "Remove a dependency between two plans.",
}, toolSet.Plans.RemoveDependency); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "add_related_plan",
Description: "Link two plans as thematically related (bidirectional).",
}, toolSet.Plans.AddRelated); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "remove_related_plan",
Description: "Unlink two related plans.",
}, toolSet.Plans.RemoveRelated); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "add_plan_skill",
Description: "Link an agent skill to a plan.",
}, toolSet.Plans.AddSkill); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "remove_plan_skill",
Description: "Unlink an agent skill from a plan.",
}, toolSet.Plans.RemoveSkill); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "list_plan_skills",
Description: "List skills linked to a plan.",
}, toolSet.Plans.ListSkills); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "add_plan_guardrail",
Description: "Link an agent guardrail to a plan.",
}, toolSet.Plans.AddGuardrail); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "remove_plan_guardrail",
Description: "Unlink an agent guardrail from a plan.",
}, toolSet.Plans.RemoveGuardrail); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "list_plan_guardrails",
Description: "List guardrails linked to a plan.",
}, toolSet.Plans.ListGuardrails); err != nil {
return err
}
return nil
}
func registerFileTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
server.AddResourceTemplate(&mcp.ResourceTemplate{
Name: "stored_file",
URITemplate: "amcs://files/{id}",
Description: "A stored file. Read a file's raw binary content by its id. Use load_file for metadata.",
}, toolSet.Files.ReadResource)
if err := addTool(server, logger, &mcp.Tool{
Name: "upload_file",
Description: "Stage a file; returns amcs://files/{id}. content_path for large/binary, content_base64 for ≤10 MB. Link now or pass URI to save_file.",
}, toolSet.Files.Upload); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "save_file",
Description: "Store and optionally link a file. content_base64 (≤10 MB) or content_uri from upload_file. >10 MB: use upload_file first.",
}, toolSet.Files.Save); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "load_file",
Description: "Fetch file metadata and content by id (UUID or amcs://files/{id}); includes embedded MCP resource.",
}, toolSet.Files.Load); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "list_files",
Description: "List stored files, optionally filtered by thought, project, or kind.",
}, toolSet.Files.List); err != nil {
return err
}
return nil
}
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. Run after model switch or bulk import.",
}, toolSet.Backfill.Handle); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "reparse_thought_metadata",
Description: "Re-extract metadata from thought content.",
}, toolSet.Reparse.Handle); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "retry_failed_metadata",
Description: "Retry pending/failed metadata extraction.",
}, toolSet.RetryMetadata.Handle); err != nil {
return err
}
// if err := addTool(server, logger, &mcp.Tool{
// Name: "add_maintenance_task",
// Description: "Create a recurring or one-time home maintenance task.",
// }, toolSet.Maintenance.AddTask); err != nil {
// return err
// }
// if err := addTool(server, logger, &mcp.Tool{
// Name: "log_maintenance",
// Description: "Log completed maintenance; updates next due date.",
// }, toolSet.Maintenance.LogWork); err != nil {
// return err
// }
// if err := addTool(server, logger, &mcp.Tool{
// Name: "get_upcoming_maintenance",
// Description: "List maintenance tasks due within the next N days.",
// }, toolSet.Maintenance.GetUpcoming); err != nil {
// return err
// }
// if err := addTool(server, logger, &mcp.Tool{
// Name: "search_maintenance_history",
// Description: "Search the maintenance log by task name, category, or date range.",
// }, toolSet.Maintenance.SearchHistory); err != nil {
// return err
// }
return nil
}
func registerSkillTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
if err := addTool(server, logger, &mcp.Tool{
Name: "add_skill",
Description: "Store an agent skill (instruction or capability prompt).",
}, toolSet.Skills.AddSkill); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "remove_skill",
Description: "Delete an agent skill by id.",
}, toolSet.Skills.RemoveSkill); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "list_skills",
Description: "List agent skills (metadata only by default). Set include_content=true to get full content, or use get_skill for a single skill.",
}, toolSet.Skills.ListSkills); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "get_skill",
Description: "Fetch a single agent skill with full content by id or name.",
}, toolSet.Skills.GetSkill); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "add_guardrail",
Description: "Store an agent guardrail (constraint or safety rule).",
}, toolSet.Skills.AddGuardrail); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "remove_guardrail",
Description: "Delete an agent guardrail by id.",
}, toolSet.Skills.RemoveGuardrail); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "list_guardrails",
Description: "List all agent guardrails, optionally filtered by tag or severity.",
}, toolSet.Skills.ListGuardrails); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "get_guardrail",
Description: "Fetch a single agent guardrail with full content by id or name.",
}, toolSet.Skills.GetGuardrail); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "add_project_skill",
Description: "Link a skill to a project. Pass project if client is stateless.",
}, toolSet.Skills.AddProjectSkill); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "remove_project_skill",
Description: "Unlink a skill from a project. Pass project if client is stateless.",
}, toolSet.Skills.RemoveProjectSkill); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "list_project_skills",
Description: "Skills for a project. Load at session start; only add new if none returned. Pass project if stateless.",
}, toolSet.Skills.ListProjectSkills); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "add_project_guardrail",
Description: "Link a guardrail to a project. Pass project if client is stateless.",
}, toolSet.Skills.AddProjectGuardrail); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "remove_project_guardrail",
Description: "Unlink a guardrail from a project. Pass project if client is stateless.",
}, toolSet.Skills.RemoveProjectGuardrail); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "list_project_guardrails",
Description: "Guardrails for a project. Load at session start; only add new if none returned. Pass project if stateless.",
}, toolSet.Skills.ListProjectGuardrails); err != nil {
return err
}
return nil
}
func registerChatHistoryTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
if err := addTool(server, logger, &mcp.Tool{
Name: "save_chat_history",
Description: "Save chat messages with optional title, summary, channel, agent, and project.",
}, toolSet.ChatHistory.SaveChatHistory); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "get_chat_history",
Description: "Fetch chat history by UUID or session_id.",
}, toolSet.ChatHistory.GetChatHistory); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "list_chat_histories",
Description: "List chat histories; filter by project, channel, agent_id, session_id, or days.",
}, toolSet.ChatHistory.ListChatHistories); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "delete_chat_history",
Description: "Delete a chat history by id.",
}, toolSet.ChatHistory.DeleteChatHistory); err != nil {
return err
}
return nil
}
func registerPersonaTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
p := toolSet.Personas
if err := addTool(server, logger, &mcp.Tool{Name: "create_agent_persona", Description: "Create a named, loadable agent persona."}, p.CreatePersona); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "update_agent_persona", Description: "Update persona fields (name, description, summary, detail, tags)."}, p.UpdatePersona); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "delete_agent_persona", Description: "Delete a persona by name."}, p.DeletePersona); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "list_agent_personas", Description: "List all personas, optionally filtered by tag."}, p.ListPersonas); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "get_agent_persona", Description: "Load a persona with assembled parts. detail=true returns full content. overrides replaces parts per type at runtime."}, p.GetPersona); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "get_persona_manifest", Description: "Lightweight structure-only view: parts, traits, skills, guardrails — no content. Includes on_demand_tools hints."}, p.GetPersonaManifest); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "compile_persona", Description: "Regenerate compiled_summary and compiled_detail from current parts and arc stage. Use compiled_summary for agents with tight context budgets."}, p.CompilePersona); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "create_agent_part", Description: "Create a reusable behaviour building block. Parts compose personas and can be overridden at load time by name."}, p.CreatePart); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "update_agent_part", Description: "Update part fields (name, part_type, description, summary, content, tags)."}, p.UpdatePart); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "delete_agent_part", Description: "Delete a part by name."}, p.DeletePart); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "list_agent_parts", Description: "List parts, optionally filtered by part_type or tag."}, p.ListParts); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "get_agent_part", Description: "Fetch a single part with full content by name."}, p.GetPart); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "add_persona_part", Description: "Link a part to a persona with optional order and priority."}, p.AddPersonaPart); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "remove_persona_part", Description: "Unlink a part from a persona."}, p.RemovePersonaPart); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "add_persona_skill", Description: "Link an agent_skill to a persona."}, p.AddPersonaSkill); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "remove_persona_skill", Description: "Unlink an agent_skill from a persona."}, p.RemovePersonaSkill); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "add_persona_guardrail", Description: "Link an agent_guardrail to a persona."}, p.AddPersonaGuardrail); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "remove_persona_guardrail", Description: "Unlink an agent_guardrail from a persona."}, p.RemovePersonaGuardrail); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "create_agent_trait", Description: "Create an atomic personality trait (personality, cognitive, emotional, social, behavioral)."}, p.CreateTrait); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "update_agent_trait", Description: "Update trait fields."}, p.UpdateTrait); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "delete_agent_trait", Description: "Delete a trait by name."}, p.DeleteTrait); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "list_agent_traits", Description: "List traits, optionally filtered by trait_type or tag."}, p.ListTraits); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "get_agent_trait", Description: "Fetch a single trait with instruction by name."}, p.GetTrait); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "add_persona_trait", Description: "Link a trait to a persona."}, p.AddPersonaTrait); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "remove_persona_trait", Description: "Unlink a trait from a persona."}, p.RemovePersonaTrait); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "create_character_arc", Description: "Define a named character progression arc."}, p.CreateCharacterArc); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "list_character_arcs", Description: "List all character arcs."}, p.ListCharacterArcs); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "add_arc_stage", Description: "Add an ordered stage to a character arc."}, p.AddArcStage); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "add_stage_part", Description: "Link a part to an arc stage — active when the persona is at that stage."}, p.AddStagePart); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "remove_stage_part", Description: "Unlink a part from an arc stage."}, p.RemoveStagePart); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "assign_persona_arc", Description: "Attach an arc to a persona and set the starting stage."}, p.AssignPersonaArc); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "advance_persona_stage", Description: "Move persona to the next stage in its arc."}, p.AdvancePersonaStage); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "reset_persona_stage", Description: "Reset persona to the first stage of its arc."}, p.ResetPersonaStage); err != nil {
return err
}
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 first each session. All tools with categories and usage notes. Categories: system, thoughts, projects, files, admin, maintenance, skills, personas, plans, chat, meta.",
}, toolSet.Describe.Describe); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "annotate_tool",
Description: "Save usage notes for a tool; returned by describe_tools. Empty string clears.",
}, 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"},
// learnings
{Name: "add_learning", Description: "Create a curated learning record distinct from raw thoughts.", Category: "projects"},
{Name: "get_learning", Description: "Retrieve a structured learning by id.", Category: "projects"},
{Name: "list_learnings", Description: "List structured learnings with optional project, category, area, status, priority, tag, and text filters.", Category: "projects"},
// plans
{Name: "create_plan", Description: "Create a structured plan with status, priority, owner, due date, and optional project link.", Category: "plans"},
{Name: "get_plan", Description: "Retrieve a full plan including dependencies (depends_on/blocks), related plans, linked skills, and guardrails.", Category: "plans"},
{Name: "update_plan", Description: "Partially update a plan; only provided fields are changed. Use mark_reviewed to stamp last_reviewed_at.", Category: "plans"},
{Name: "delete_plan", Description: "Hard-delete a plan by id.", Category: "plans"},
{Name: "list_plans", Description: "List plans with optional filters: project, status, priority, owner, tag, and full-text query.", Category: "plans"},
{Name: "add_plan_dependency", Description: "Declare that plan_id cannot proceed until depends_on_plan_id is complete.", Category: "plans"},
{Name: "remove_plan_dependency", Description: "Remove a directional dependency between two plans.", Category: "plans"},
{Name: "add_related_plan", Description: "Link two plans as thematically related (bidirectional, order-independent).", Category: "plans"},
{Name: "remove_related_plan", Description: "Unlink two related plans.", Category: "plans"},
{Name: "add_plan_skill", Description: "Link an agent skill to a plan so it is loaded with the plan's context.", Category: "plans"},
{Name: "remove_plan_skill", Description: "Unlink an agent skill from a plan.", Category: "plans"},
{Name: "list_plan_skills", Description: "List all skills linked to a plan.", Category: "plans"},
{Name: "add_plan_guardrail", Description: "Link an agent guardrail to a plan so it applies during plan execution.", Category: "plans"},
{Name: "remove_plan_guardrail", Description: "Unlink an agent guardrail from a plan.", Category: "plans"},
{Name: "list_plan_guardrails", Description: "List all guardrails linked to a plan.", Category: "plans"},
// 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"},
// 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"},
// skills
{Name: "add_skill", Description: "Store a reusable agent skill. Supports language_tags, library_tags, framework_tags, and domain_tags for precise retrieval.", Category: "skills"},
{Name: "remove_skill", Description: "Delete an agent skill by id.", Category: "skills"},
{Name: "list_skills", Description: "List agent skills (metadata only by default). Filter by tag (searches all tag fields). Set include_content=true for full bodies, or use get_skill to load one.", Category: "skills"},
{Name: "get_skill", Description: "Fetch a single agent skill with full content by id or name. Prefer this over list_skills when you know which skill you need.", 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: "get_guardrail", Description: "Fetch a single agent guardrail with full content by id or name.", 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"},
// personas
{Name: "create_agent_persona", Description: "Create a named, loadable agent persona that composes parts, traits, skills, and guardrails.", Category: "personas"},
{Name: "update_agent_persona", Description: "Update persona fields.", Category: "personas"},
{Name: "delete_agent_persona", Description: "Delete a persona by name.", Category: "personas"},
{Name: "list_agent_personas", Description: "List all personas, optionally filtered by tag.", Category: "personas"},
{Name: "get_agent_persona", Description: "Load a persona with all assembled parts. detail=true returns full content. overrides replaces parts per type at runtime without modifying the persona.", Category: "personas"},
{Name: "get_persona_manifest", Description: "Lightweight discovery: returns persona structure (parts, traits, skills, guardrails) with no content, plus on_demand_tools hints. Call this first for unknown personas.", Category: "personas"},
{Name: "compile_persona", Description: "Regenerate compiled_summary and compiled_detail from current parts and arc stage. Agents with tight context budgets can use compiled_summary directly.", Category: "personas"},
{Name: "create_agent_part", Description: "Create a reusable behaviour building block. Part types: system, agent, soul, identity, skill, specialization, tone, goal, context, protocol, backstory, motivation, voice, archetype, flaw, relationship.", Category: "personas"},
{Name: "update_agent_part", Description: "Update part fields.", Category: "personas"},
{Name: "delete_agent_part", Description: "Delete a part by name.", Category: "personas"},
{Name: "list_agent_parts", Description: "List parts, optionally filtered by part_type or tag.", Category: "personas"},
{Name: "get_agent_part", Description: "Fetch a single part with full content by name.", Category: "personas"},
{Name: "add_persona_part", Description: "Link a part to a persona with optional order and priority.", Category: "personas"},
{Name: "remove_persona_part", Description: "Unlink a part from a persona.", Category: "personas"},
{Name: "add_persona_skill", Description: "Link an agent_skill to a persona.", Category: "personas"},
{Name: "remove_persona_skill", Description: "Unlink an agent_skill from a persona.", Category: "personas"},
{Name: "add_persona_guardrail", Description: "Link an agent_guardrail to a persona.", Category: "personas"},
{Name: "remove_persona_guardrail", Description: "Unlink an agent_guardrail from a persona.", Category: "personas"},
{Name: "create_agent_trait", Description: "Create an atomic personality trait. Trait types: personality, cognitive, emotional, social, behavioral.", Category: "personas"},
{Name: "update_agent_trait", Description: "Update trait fields.", Category: "personas"},
{Name: "delete_agent_trait", Description: "Delete a trait by name.", Category: "personas"},
{Name: "list_agent_traits", Description: "List traits, optionally filtered by trait_type or tag.", Category: "personas"},
{Name: "get_agent_trait", Description: "Fetch a single trait with instruction by name. Use this for on-demand trait loading.", Category: "personas"},
{Name: "add_persona_trait", Description: "Link a trait to a persona.", Category: "personas"},
{Name: "remove_persona_trait", Description: "Unlink a trait from a persona.", Category: "personas"},
{Name: "create_character_arc", Description: "Define a named character progression arc with ordered stages.", Category: "personas"},
{Name: "list_character_arcs", Description: "List all character arcs.", Category: "personas"},
{Name: "add_arc_stage", Description: "Add an ordered stage to a character arc.", Category: "personas"},
{Name: "add_stage_part", Description: "Link a part to an arc stage — overrides matching persona parts when that stage is active.", Category: "personas"},
{Name: "remove_stage_part", Description: "Unlink a part from an arc stage.", Category: "personas"},
{Name: "assign_persona_arc", Description: "Attach a character arc to a persona and set the starting stage.", Category: "personas"},
{Name: "advance_persona_stage", Description: "Move persona to the next stage in its arc.", Category: "personas"},
{Name: "reset_persona_stage", Description: "Reset persona to the first stage of its arc.", Category: "personas"},
// 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, personas, plans, 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"},
}
}