feat: add skill registry with tool routing
This commit is contained in:
50
internal/registry/registry.go
Normal file
50
internal/registry/registry.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToolDef describes a single MCP tool exposed by a skill.
|
||||||
|
type ToolDef struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
InputSchema json.RawMessage `json:"inputSchema"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skill is implemented by each skill package (tdd, review, etc.).
|
||||||
|
type Skill interface {
|
||||||
|
Name() string
|
||||||
|
Tools() []ToolDef
|
||||||
|
Handle(ctx context.Context, tool string, args json.RawMessage) (json.RawMessage, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registry routes MCP tool calls to the correct skill handler.
|
||||||
|
type Registry struct {
|
||||||
|
skills map[string]Skill // tool name → skill
|
||||||
|
tools []ToolDef
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *Registry {
|
||||||
|
return &Registry{skills: make(map[string]Skill)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) Register(s Skill) {
|
||||||
|
for _, t := range s.Tools() {
|
||||||
|
r.skills[t.Name] = s
|
||||||
|
r.tools = append(r.tools, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) Tools() []ToolDef {
|
||||||
|
return r.tools
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) Dispatch(ctx context.Context, tool string, args json.RawMessage) (json.RawMessage, error) {
|
||||||
|
s, ok := r.skills[tool]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unknown tool: %s", tool)
|
||||||
|
}
|
||||||
|
return s.Handle(ctx, tool, args)
|
||||||
|
}
|
||||||
51
internal/registry/registry_test.go
Normal file
51
internal/registry/registry_test.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package registry_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mathiasbq/supervisor/internal/registry"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stubSkill struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s stubSkill) Name() string { return s.name }
|
||||||
|
func (s stubSkill) Tools() []registry.ToolDef {
|
||||||
|
return []registry.ToolDef{{
|
||||||
|
Name: s.name + "_tool",
|
||||||
|
Description: "stub tool",
|
||||||
|
InputSchema: json.RawMessage(`{"type":"object","properties":{}}`),
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
func (s stubSkill) Handle(ctx context.Context, tool string, args json.RawMessage) (json.RawMessage, error) {
|
||||||
|
return json.RawMessage(`{"ok":true}`), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegistryRoutes(t *testing.T) {
|
||||||
|
r := registry.New()
|
||||||
|
r.Register(stubSkill{name: "tdd"})
|
||||||
|
|
||||||
|
result, err := r.Dispatch(context.Background(), "tdd_tool", json.RawMessage(`{}`))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.JSONEq(t, `{"ok":true}`, string(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegistryUnknownTool(t *testing.T) {
|
||||||
|
r := registry.New()
|
||||||
|
_, err := r.Dispatch(context.Background(), "unknown_tool", json.RawMessage(`{}`))
|
||||||
|
assert.ErrorContains(t, err, "unknown tool")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegistryListsTools(t *testing.T) {
|
||||||
|
r := registry.New()
|
||||||
|
r.Register(stubSkill{name: "tdd"})
|
||||||
|
|
||||||
|
tools := r.Tools()
|
||||||
|
require.Len(t, tools, 1)
|
||||||
|
assert.Equal(t, "tdd_tool", tools[0].Name)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user