Files
amcs/internal/tools/skills.go
Hein (Warky) 1ceb317f4b
Some checks failed
CI / build-and-test (push) Failing after -29m56s
feat(skills): enhance agent skills with additional tags and commands
- Added language_tags, library_tags, framework_tags, and domain_tags to agent skills.
- Introduced new commands: get_skill and get_guardrail for fetching specific skills and guardrails.
- Updated database schema and migration scripts to accommodate new fields.
- Enhanced skill listing functionality to support filtering by new tags.
- Improved error handling and response structures for skill-related operations.
2026-05-05 09:24:58 +02:00

400 lines
14 KiB
Go

package tools
import (
"context"
"strings"
"github.com/modelcontextprotocol/go-sdk/mcp"
"git.warky.dev/wdevs/amcs/internal/session"
"git.warky.dev/wdevs/amcs/internal/store"
ext "git.warky.dev/wdevs/amcs/internal/types"
)
type SkillsTool struct {
store *store.DB
sessions *session.ActiveProjects
}
func NewSkillsTool(db *store.DB, sessions *session.ActiveProjects) *SkillsTool {
return &SkillsTool{store: db, sessions: sessions}
}
// add_skill
type AddSkillInput struct {
Name string `json:"name" jsonschema:"unique skill name"`
Description string `json:"description,omitempty" jsonschema:"short description of what the skill does"`
Content string `json:"content" jsonschema:"the full skill instruction or prompt content"`
Tags []string `json:"tags,omitempty" jsonschema:"general tags for grouping or filtering"`
LanguageTags []string `json:"language_tags,omitempty" jsonschema:"programming languages this skill applies to (e.g. go, python, typescript)"`
LibraryTags []string `json:"library_tags,omitempty" jsonschema:"libraries or packages this skill applies to (e.g. bunrouter, pgx, axios)"`
FrameworkTags []string `json:"framework_tags,omitempty" jsonschema:"frameworks this skill applies to (e.g. gin, nextjs, django)"`
DomainTags []string `json:"domain_tags,omitempty" jsonschema:"topic or problem domains this skill applies to (e.g. auth, testing, migrations)"`
}
type AddSkillOutput struct {
Skill ext.AgentSkill `json:"skill"`
}
func (t *SkillsTool) AddSkill(ctx context.Context, _ *mcp.CallToolRequest, in AddSkillInput) (*mcp.CallToolResult, AddSkillOutput, error) {
if strings.TrimSpace(in.Name) == "" {
return nil, AddSkillOutput{}, errRequiredField("name")
}
if strings.TrimSpace(in.Content) == "" {
return nil, AddSkillOutput{}, errRequiredField("content")
}
if in.Tags == nil {
in.Tags = []string{}
}
skill, err := t.store.AddSkill(ctx, ext.AgentSkill{
Name: strings.TrimSpace(in.Name),
Description: strings.TrimSpace(in.Description),
Content: strings.TrimSpace(in.Content),
Tags: in.Tags,
LanguageTags: in.LanguageTags,
LibraryTags: in.LibraryTags,
FrameworkTags: in.FrameworkTags,
DomainTags: in.DomainTags,
})
if err != nil {
return nil, AddSkillOutput{}, err
}
return nil, AddSkillOutput{Skill: skill}, nil
}
// remove_skill
type RemoveSkillInput struct {
ID int64 `json:"id" jsonschema:"skill id to remove"`
}
type RemoveSkillOutput struct {
Removed bool `json:"removed"`
}
func (t *SkillsTool) RemoveSkill(ctx context.Context, _ *mcp.CallToolRequest, in RemoveSkillInput) (*mcp.CallToolResult, RemoveSkillOutput, error) {
if err := t.store.RemoveSkill(ctx, in.ID); err != nil {
return nil, RemoveSkillOutput{}, err
}
return nil, RemoveSkillOutput{Removed: true}, nil
}
// get_skill
type GetSkillInput struct {
ID int64 `json:"id,omitempty" jsonschema:"skill id (preferred; use instead of name when available)"`
Name string `json:"name,omitempty" jsonschema:"skill name (alternative to id)"`
}
type GetSkillOutput struct {
Skill ext.AgentSkill `json:"skill"`
}
func (t *SkillsTool) GetSkill(ctx context.Context, _ *mcp.CallToolRequest, in GetSkillInput) (*mcp.CallToolResult, GetSkillOutput, error) {
var (
skill ext.AgentSkill
err error
)
switch {
case in.ID != 0:
skill, err = t.store.GetSkill(ctx, in.ID)
case strings.TrimSpace(in.Name) != "":
skill, err = t.store.GetSkillByName(ctx, strings.TrimSpace(in.Name))
default:
return nil, GetSkillOutput{}, errRequiredField("id or name")
}
if err != nil {
return nil, GetSkillOutput{}, err
}
return nil, GetSkillOutput{Skill: skill}, nil
}
// list_skills
type ListSkillsInput struct {
Tag string `json:"tag,omitempty" jsonschema:"filter by tag (searches all tag fields: tags, language_tags, library_tags, framework_tags, domain_tags)"`
IncludeContent bool `json:"include_content,omitempty" jsonschema:"include full skill content in results (default false — use get_skill to load content for a specific skill)"`
}
type ListSkillsOutput struct {
Skills []ext.AgentSkill `json:"skills"`
}
func (t *SkillsTool) ListSkills(ctx context.Context, _ *mcp.CallToolRequest, in ListSkillsInput) (*mcp.CallToolResult, ListSkillsOutput, error) {
skills, err := t.store.ListSkills(ctx, in.Tag)
if err != nil {
return nil, ListSkillsOutput{}, err
}
if skills == nil {
skills = []ext.AgentSkill{}
}
if !in.IncludeContent {
for i := range skills {
skills[i].Content = ""
}
}
return nil, ListSkillsOutput{Skills: skills}, nil
}
// add_guardrail
type AddGuardrailInput struct {
Name string `json:"name" jsonschema:"unique guardrail name"`
Description string `json:"description,omitempty" jsonschema:"short description of what the guardrail enforces"`
Content string `json:"content" jsonschema:"the full guardrail rule or constraint content"`
Severity string `json:"severity,omitempty" jsonschema:"one of: low, medium, high, critical (default medium)"`
Tags []string `json:"tags,omitempty" jsonschema:"optional tags for grouping or filtering"`
}
type AddGuardrailOutput struct {
Guardrail ext.AgentGuardrail `json:"guardrail"`
}
func (t *SkillsTool) AddGuardrail(ctx context.Context, _ *mcp.CallToolRequest, in AddGuardrailInput) (*mcp.CallToolResult, AddGuardrailOutput, error) {
if strings.TrimSpace(in.Name) == "" {
return nil, AddGuardrailOutput{}, errRequiredField("name")
}
if strings.TrimSpace(in.Content) == "" {
return nil, AddGuardrailOutput{}, errRequiredField("content")
}
severity := strings.TrimSpace(in.Severity)
if severity == "" {
severity = "medium"
}
switch severity {
case "low", "medium", "high", "critical":
default:
return nil, AddGuardrailOutput{}, errInvalidField(
"severity",
"severity must be one of: low, medium, high, critical",
"pass one of: low, medium, high, critical",
)
}
if in.Tags == nil {
in.Tags = []string{}
}
guardrail, err := t.store.AddGuardrail(ctx, ext.AgentGuardrail{
Name: strings.TrimSpace(in.Name),
Description: strings.TrimSpace(in.Description),
Content: strings.TrimSpace(in.Content),
Severity: severity,
Tags: in.Tags,
})
if err != nil {
return nil, AddGuardrailOutput{}, err
}
return nil, AddGuardrailOutput{Guardrail: guardrail}, nil
}
// remove_guardrail
type RemoveGuardrailInput struct {
ID int64 `json:"id" jsonschema:"guardrail id to remove"`
}
type RemoveGuardrailOutput struct {
Removed bool `json:"removed"`
}
func (t *SkillsTool) RemoveGuardrail(ctx context.Context, _ *mcp.CallToolRequest, in RemoveGuardrailInput) (*mcp.CallToolResult, RemoveGuardrailOutput, error) {
if err := t.store.RemoveGuardrail(ctx, in.ID); err != nil {
return nil, RemoveGuardrailOutput{}, err
}
return nil, RemoveGuardrailOutput{Removed: true}, nil
}
// list_guardrails
type ListGuardrailsInput struct {
Tag string `json:"tag,omitempty" jsonschema:"filter by tag"`
Severity string `json:"severity,omitempty" jsonschema:"filter by severity: low, medium, high, or critical"`
}
type ListGuardrailsOutput struct {
Guardrails []ext.AgentGuardrail `json:"guardrails"`
}
func (t *SkillsTool) ListGuardrails(ctx context.Context, _ *mcp.CallToolRequest, in ListGuardrailsInput) (*mcp.CallToolResult, ListGuardrailsOutput, error) {
guardrails, err := t.store.ListGuardrails(ctx, in.Tag, in.Severity)
if err != nil {
return nil, ListGuardrailsOutput{}, err
}
if guardrails == nil {
guardrails = []ext.AgentGuardrail{}
}
return nil, ListGuardrailsOutput{Guardrails: guardrails}, nil
}
// get_guardrail
type GetGuardrailInput struct {
ID int64 `json:"id,omitempty" jsonschema:"guardrail id (preferred; use instead of name when available)"`
Name string `json:"name,omitempty" jsonschema:"guardrail name (alternative to id)"`
}
type GetGuardrailOutput struct {
Guardrail ext.AgentGuardrail `json:"guardrail"`
}
func (t *SkillsTool) GetGuardrail(ctx context.Context, _ *mcp.CallToolRequest, in GetGuardrailInput) (*mcp.CallToolResult, GetGuardrailOutput, error) {
var (
g ext.AgentGuardrail
err error
)
switch {
case in.ID != 0:
g, err = t.store.GetGuardrail(ctx, in.ID)
case strings.TrimSpace(in.Name) != "":
g, err = t.store.GetGuardrailByName(ctx, strings.TrimSpace(in.Name))
default:
return nil, GetGuardrailOutput{}, errRequiredField("id or name")
}
if err != nil {
return nil, GetGuardrailOutput{}, err
}
return nil, GetGuardrailOutput{Guardrail: g}, nil
}
// add_project_skill
type AddProjectSkillInput struct {
Project string `json:"project,omitempty" jsonschema:"project name or id (uses active project if omitted)"`
SkillID int64 `json:"skill_id" jsonschema:"skill id to link"`
}
type AddProjectSkillOutput struct {
ProjectID int64 `json:"project_id"`
SkillID int64 `json:"skill_id"`
}
func (t *SkillsTool) AddProjectSkill(ctx context.Context, req *mcp.CallToolRequest, in AddProjectSkillInput) (*mcp.CallToolResult, AddProjectSkillOutput, error) {
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, true)
if err != nil {
return nil, AddProjectSkillOutput{}, err
}
if err := t.store.AddProjectSkill(ctx, project.NumericID, in.SkillID); err != nil {
return nil, AddProjectSkillOutput{}, err
}
return nil, AddProjectSkillOutput{ProjectID: project.NumericID, SkillID: in.SkillID}, nil
}
// remove_project_skill
type RemoveProjectSkillInput struct {
Project string `json:"project,omitempty" jsonschema:"project name or id (uses active project if omitted)"`
SkillID int64 `json:"skill_id" jsonschema:"skill id to unlink"`
}
type RemoveProjectSkillOutput struct {
Removed bool `json:"removed"`
}
func (t *SkillsTool) RemoveProjectSkill(ctx context.Context, req *mcp.CallToolRequest, in RemoveProjectSkillInput) (*mcp.CallToolResult, RemoveProjectSkillOutput, error) {
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, true)
if err != nil {
return nil, RemoveProjectSkillOutput{}, err
}
if err := t.store.RemoveProjectSkill(ctx, project.NumericID, in.SkillID); err != nil {
return nil, RemoveProjectSkillOutput{}, err
}
return nil, RemoveProjectSkillOutput{Removed: true}, nil
}
// list_project_skills
type ListProjectSkillsInput struct {
Project string `json:"project,omitempty" jsonschema:"project name or id (uses active project if omitted)"`
}
type ListProjectSkillsOutput struct {
ProjectID int64 `json:"project_id"`
Skills []ext.AgentSkill `json:"skills"`
}
func (t *SkillsTool) ListProjectSkills(ctx context.Context, req *mcp.CallToolRequest, in ListProjectSkillsInput) (*mcp.CallToolResult, ListProjectSkillsOutput, error) {
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, true)
if err != nil {
return nil, ListProjectSkillsOutput{}, err
}
skills, err := t.store.ListProjectSkills(ctx, project.NumericID)
if err != nil {
return nil, ListProjectSkillsOutput{}, err
}
if skills == nil {
skills = []ext.AgentSkill{}
}
return nil, ListProjectSkillsOutput{ProjectID: project.NumericID, Skills: skills}, nil
}
// add_project_guardrail
type AddProjectGuardrailInput struct {
Project string `json:"project,omitempty" jsonschema:"project name or id (uses active project if omitted)"`
GuardrailID int64 `json:"guardrail_id" jsonschema:"guardrail id to link"`
}
type AddProjectGuardrailOutput struct {
ProjectID int64 `json:"project_id"`
GuardrailID int64 `json:"guardrail_id"`
}
func (t *SkillsTool) AddProjectGuardrail(ctx context.Context, req *mcp.CallToolRequest, in AddProjectGuardrailInput) (*mcp.CallToolResult, AddProjectGuardrailOutput, error) {
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, true)
if err != nil {
return nil, AddProjectGuardrailOutput{}, err
}
if err := t.store.AddProjectGuardrail(ctx, project.NumericID, in.GuardrailID); err != nil {
return nil, AddProjectGuardrailOutput{}, err
}
return nil, AddProjectGuardrailOutput{ProjectID: project.NumericID, GuardrailID: in.GuardrailID}, nil
}
// remove_project_guardrail
type RemoveProjectGuardrailInput struct {
Project string `json:"project,omitempty" jsonschema:"project name or id (uses active project if omitted)"`
GuardrailID int64 `json:"guardrail_id" jsonschema:"guardrail id to unlink"`
}
type RemoveProjectGuardrailOutput struct {
Removed bool `json:"removed"`
}
func (t *SkillsTool) RemoveProjectGuardrail(ctx context.Context, req *mcp.CallToolRequest, in RemoveProjectGuardrailInput) (*mcp.CallToolResult, RemoveProjectGuardrailOutput, error) {
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, true)
if err != nil {
return nil, RemoveProjectGuardrailOutput{}, err
}
if err := t.store.RemoveProjectGuardrail(ctx, project.NumericID, in.GuardrailID); err != nil {
return nil, RemoveProjectGuardrailOutput{}, err
}
return nil, RemoveProjectGuardrailOutput{Removed: true}, nil
}
// list_project_guardrails
type ListProjectGuardrailsInput struct {
Project string `json:"project,omitempty" jsonschema:"project name or id (uses active project if omitted)"`
}
type ListProjectGuardrailsOutput struct {
ProjectID int64 `json:"project_id"`
Guardrails []ext.AgentGuardrail `json:"guardrails"`
}
func (t *SkillsTool) ListProjectGuardrails(ctx context.Context, req *mcp.CallToolRequest, in ListProjectGuardrailsInput) (*mcp.CallToolResult, ListProjectGuardrailsOutput, error) {
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, true)
if err != nil {
return nil, ListProjectGuardrailsOutput{}, err
}
guardrails, err := t.store.ListProjectGuardrails(ctx, project.NumericID)
if err != nil {
return nil, ListProjectGuardrailsOutput{}, err
}
if guardrails == nil {
guardrails = []ext.AgentGuardrail{}
}
return nil, ListProjectGuardrailsOutput{ProjectID: project.NumericID, Guardrails: guardrails}, nil
}