feat(assistant): add hot-reload functionality for assistants and enhance gRPC metadata handling
- Implemented AssistantReloadFunc to enable hot-reloading of assistants after deployment, improving deployment flexibility. - Enhanced gRPC authProvider to include workspace and sandbox IDs from incoming context metadata, enriching the authentication context. - Updated tools to support new agent-related functionalities, including listing, downloading, deploying, and connecting agents. - Expanded system tools documentation to include new agent commands, ensuring comprehensive guidance for users.
This commit is contained in:
parent
6af83149ef
commit
639f0c59fc
18 changed files with 1032 additions and 16 deletions
|
|
@ -3,6 +3,7 @@ package assistant
|
|||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/yaoapp/gou/fs"
|
||||
"github.com/yaoapp/yao/agent/caller"
|
||||
|
|
@ -27,6 +28,28 @@ func init() {
|
|||
return &agentCallerWrapper{ast: ast}, nil
|
||||
}
|
||||
|
||||
// Initialize AssistantReloadFunc for hot-reload after deploy
|
||||
caller.AssistantReloadFunc = func(id string) error {
|
||||
p := "/assistants/" + strings.Replace(id, ".", "/", 1)
|
||||
ast, err := LoadPath(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ast.BuiltIn = true
|
||||
ast.Readonly = true
|
||||
if ast.Tags == nil {
|
||||
ast.Tags = []string{}
|
||||
}
|
||||
if err := ast.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ast.initialize(); err != nil {
|
||||
return err
|
||||
}
|
||||
loaded.Put(ast)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Initialize Agent JSAPI factory for ctx.agent.* methods
|
||||
caller.SetJSAPIFactory()
|
||||
|
||||
|
|
|
|||
|
|
@ -15,3 +15,7 @@ type AgentCaller interface {
|
|||
// AgentGetterFunc is a function type that gets an agent by ID
|
||||
// This should be set by the assistant package during initialization
|
||||
var AgentGetterFunc func(agentID string) (AgentCaller, error)
|
||||
|
||||
// AssistantReloadFunc reloads a single assistant from disk after deploy.
|
||||
// Set by the assistant package during initialization.
|
||||
var AssistantReloadFunc func(id string) error
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
goumcp "github.com/yaoapp/gou/mcp"
|
||||
|
|
@ -28,7 +29,7 @@ func authProviderFromCtx(ctx context.Context) *grpcAuthProvider {
|
|||
if info == nil {
|
||||
return nil
|
||||
}
|
||||
return &grpcAuthProvider{m: map[string]interface{}{
|
||||
m := map[string]interface{}{
|
||||
"sub": info.Subject,
|
||||
"client_id": info.ClientID,
|
||||
"scope": info.Scope,
|
||||
|
|
@ -36,7 +37,16 @@ func authProviderFromCtx(ctx context.Context) *grpcAuthProvider {
|
|||
"user_id": info.UserID,
|
||||
"team_id": info.TeamID,
|
||||
"tenant_id": info.TenantID,
|
||||
}}
|
||||
}
|
||||
if md, ok := metadata.FromIncomingContext(ctx); ok {
|
||||
if ids := md.Get("x-workspace-id"); len(ids) > 0 && ids[0] != "" {
|
||||
m["workspace_id"] = ids[0]
|
||||
}
|
||||
if ids := md.Get("x-sandbox-id"); len(ids) > 0 && ids[0] != "" {
|
||||
m["sandbox_id"] = ids[0]
|
||||
}
|
||||
}
|
||||
return &grpcAuthProvider{m: m}
|
||||
}
|
||||
|
||||
// MCPListTools lists all available MCP tools for a given session.
|
||||
|
|
|
|||
111
tools/agent/agent.go
Normal file
111
tools/agent/agent.go
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/yaoapp/gou/process"
|
||||
taiworkspace "github.com/yaoapp/yao/tai/workspace"
|
||||
ws "github.com/yaoapp/yao/workspace"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
//go:embed list_schema.json
|
||||
var ListSchemaJSON []byte
|
||||
|
||||
//go:embed download_schema.json
|
||||
var DownloadSchemaJSON []byte
|
||||
|
||||
//go:embed deploy_schema.json
|
||||
var DeploySchemaJSON []byte
|
||||
|
||||
//go:embed connectors_schema.json
|
||||
var ConnectorsSchemaJSON []byte
|
||||
|
||||
const allowedDeployNamespace = "smith"
|
||||
|
||||
type agentInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Capabilities string `json:"capabilities,omitempty"`
|
||||
}
|
||||
|
||||
type packageDSL struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Capabilities string `json:"capabilities"`
|
||||
}
|
||||
|
||||
func resolveWorkspaceFS(proc *process.Process) (taiworkspace.FS, error) {
|
||||
workspaceID := extractWorkspaceID(proc)
|
||||
if workspaceID == "" {
|
||||
return nil, fmt.Errorf("workspace_id not available (container must set CTX_WORKSPACE_ID)")
|
||||
}
|
||||
|
||||
fs, err := ws.M().FS(context.Background(), workspaceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("workspace %s: %w", workspaceID, err)
|
||||
}
|
||||
return fs, nil
|
||||
}
|
||||
|
||||
func extractWorkspaceID(proc *process.Process) string {
|
||||
if proc.Context == nil {
|
||||
return ""
|
||||
}
|
||||
md, ok := metadata.FromIncomingContext(proc.Context)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
ids := md.Get("x-workspace-id")
|
||||
if len(ids) > 0 && ids[0] != "" {
|
||||
return ids[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func validateID(id string) error {
|
||||
if strings.Contains(id, "..") {
|
||||
return fmt.Errorf("invalid id: path traversal not allowed")
|
||||
}
|
||||
if strings.ContainsAny(id, "/\\") {
|
||||
return fmt.Errorf("invalid id: use dot notation (e.g. 'yao.slides')")
|
||||
}
|
||||
parts := strings.SplitN(id, ".", 2)
|
||||
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
||||
return fmt.Errorf("invalid id format: expected 'namespace.name' (e.g. 'yao.slides')")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func idToPath(id string) string {
|
||||
return strings.Replace(id, ".", "/", 1)
|
||||
}
|
||||
|
||||
func settingStr(setting map[string]interface{}, key string) string {
|
||||
if v, ok := setting[key]; ok {
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func sanitizeCapabilities(caps interface{}) interface{} {
|
||||
data, err := json.Marshal(caps)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
return caps
|
||||
}
|
||||
delete(m, "key")
|
||||
delete(m, "secret")
|
||||
delete(m, "token")
|
||||
return m
|
||||
}
|
||||
448
tools/agent/agent_test.go
Normal file
448
tools/agent/agent_test.go
Normal file
|
|
@ -0,0 +1,448 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/yaoapp/gou/process"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/llmprovider"
|
||||
"github.com/yaoapp/yao/setting"
|
||||
"github.com/yaoapp/yao/test"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
test.Prepare(nil, config.Conf)
|
||||
defer test.Clean()
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
// --- Pure function tests (no app environment needed) ---
|
||||
|
||||
func TestValidateID_Valid(t *testing.T) {
|
||||
valid := []string{
|
||||
"yao.slides",
|
||||
"smith.weather",
|
||||
"ns.agent-name",
|
||||
"a.b",
|
||||
}
|
||||
for _, id := range valid {
|
||||
if err := validateID(id); err != nil {
|
||||
t.Errorf("validateID(%q) unexpected error: %v", id, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateID_Invalid(t *testing.T) {
|
||||
cases := []struct {
|
||||
id string
|
||||
want string
|
||||
}{
|
||||
{"", "invalid id format"},
|
||||
{"nodot", "invalid id format"},
|
||||
{".leading", "invalid id format"},
|
||||
{"trailing.", "invalid id format"},
|
||||
{"a..b", "path traversal"},
|
||||
{"a/b", "dot notation"},
|
||||
{"a\\b", "dot notation"},
|
||||
{"ns/name.ext", "dot notation"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := validateID(tc.id)
|
||||
if err == nil {
|
||||
t.Errorf("validateID(%q) expected error containing %q, got nil", tc.id, tc.want)
|
||||
continue
|
||||
}
|
||||
if !contains(err.Error(), tc.want) {
|
||||
t.Errorf("validateID(%q) error = %q, want substring %q", tc.id, err.Error(), tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdToPath(t *testing.T) {
|
||||
cases := []struct {
|
||||
id string
|
||||
want string
|
||||
}{
|
||||
{"yao.slides", "yao/slides"},
|
||||
{"smith.weather", "smith/weather"},
|
||||
{"ns.agent.extra", "ns/agent.extra"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got := idToPath(tc.id)
|
||||
if got != tc.want {
|
||||
t.Errorf("idToPath(%q) = %q, want %q", tc.id, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingStr(t *testing.T) {
|
||||
m := map[string]interface{}{
|
||||
"key1": "value1",
|
||||
"key2": 42,
|
||||
"key3": nil,
|
||||
}
|
||||
|
||||
if v := settingStr(m, "key1"); v != "value1" {
|
||||
t.Errorf("settingStr(key1) = %q, want %q", v, "value1")
|
||||
}
|
||||
if v := settingStr(m, "key2"); v != "" {
|
||||
t.Errorf("settingStr(key2) = %q, want empty (non-string)", v)
|
||||
}
|
||||
if v := settingStr(m, "key3"); v != "" {
|
||||
t.Errorf("settingStr(key3) = %q, want empty (nil value)", v)
|
||||
}
|
||||
if v := settingStr(m, "missing"); v != "" {
|
||||
t.Errorf("settingStr(missing) = %q, want empty", v)
|
||||
}
|
||||
if v := settingStr(nil, "any"); v != "" {
|
||||
t.Errorf("settingStr(nil map) = %q, want empty", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeCapabilities(t *testing.T) {
|
||||
caps := map[string]interface{}{
|
||||
"tool_calls": true,
|
||||
"streaming": true,
|
||||
"key": "sk-secret-123",
|
||||
"secret": "my-secret",
|
||||
"token": "bearer-xyz",
|
||||
"reasoning": false,
|
||||
}
|
||||
result := sanitizeCapabilities(caps)
|
||||
m, ok := result.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected map, got %T", result)
|
||||
}
|
||||
if _, has := m["key"]; has {
|
||||
t.Error("sanitizeCapabilities should remove 'key'")
|
||||
}
|
||||
if _, has := m["secret"]; has {
|
||||
t.Error("sanitizeCapabilities should remove 'secret'")
|
||||
}
|
||||
if _, has := m["token"]; has {
|
||||
t.Error("sanitizeCapabilities should remove 'token'")
|
||||
}
|
||||
if m["tool_calls"] != true {
|
||||
t.Error("sanitizeCapabilities should preserve 'tool_calls'")
|
||||
}
|
||||
if m["streaming"] != true {
|
||||
t.Error("sanitizeCapabilities should preserve 'streaming'")
|
||||
}
|
||||
if m["reasoning"] != false {
|
||||
t.Error("sanitizeCapabilities should preserve 'reasoning'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeCapabilities_NonMap(t *testing.T) {
|
||||
result := sanitizeCapabilities("not-a-map")
|
||||
if result != "not-a-map" {
|
||||
t.Errorf("non-map input should be returned as-is, got %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeCapabilities_Nil(t *testing.T) {
|
||||
result := sanitizeCapabilities(nil)
|
||||
if m, ok := result.(map[string]interface{}); ok && m != nil {
|
||||
t.Errorf("nil input should yield nil map, got %v", m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractWorkspaceID_WithMetadata(t *testing.T) {
|
||||
md := metadata.Pairs("x-workspace-id", "ws-abc-123")
|
||||
ctx := metadata.NewIncomingContext(context.Background(), md)
|
||||
proc := &process.Process{Context: ctx}
|
||||
|
||||
id := extractWorkspaceID(proc)
|
||||
if id != "ws-abc-123" {
|
||||
t.Errorf("extractWorkspaceID = %q, want %q", id, "ws-abc-123")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractWorkspaceID_NoMetadata(t *testing.T) {
|
||||
proc := &process.Process{Context: context.Background()}
|
||||
id := extractWorkspaceID(proc)
|
||||
if id != "" {
|
||||
t.Errorf("extractWorkspaceID without metadata = %q, want empty", id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractWorkspaceID_NilContext(t *testing.T) {
|
||||
proc := &process.Process{}
|
||||
id := extractWorkspaceID(proc)
|
||||
if id != "" {
|
||||
t.Errorf("extractWorkspaceID with nil context = %q, want empty", id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractWorkspaceID_EmptyValue(t *testing.T) {
|
||||
md := metadata.Pairs("x-workspace-id", "")
|
||||
ctx := metadata.NewIncomingContext(context.Background(), md)
|
||||
proc := &process.Process{Context: ctx}
|
||||
|
||||
id := extractWorkspaceID(proc)
|
||||
if id != "" {
|
||||
t.Errorf("extractWorkspaceID with empty value = %q, want empty", id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractWorkspaceID_OtherKeys(t *testing.T) {
|
||||
md := metadata.Pairs("x-sandbox-id", "sb-123")
|
||||
ctx := metadata.NewIncomingContext(context.Background(), md)
|
||||
proc := &process.Process{Context: ctx}
|
||||
|
||||
id := extractWorkspaceID(proc)
|
||||
if id != "" {
|
||||
t.Errorf("extractWorkspaceID with wrong key = %q, want empty", id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchemaJSON_NonEmpty(t *testing.T) {
|
||||
schemas := map[string][]byte{
|
||||
"ListSchemaJSON": ListSchemaJSON,
|
||||
"DownloadSchemaJSON": DownloadSchemaJSON,
|
||||
"DeploySchemaJSON": DeploySchemaJSON,
|
||||
"ConnectorsSchemaJSON": ConnectorsSchemaJSON,
|
||||
}
|
||||
for name, data := range schemas {
|
||||
if len(data) == 0 {
|
||||
t.Errorf("%s is empty", name)
|
||||
continue
|
||||
}
|
||||
var parsed map[string]interface{}
|
||||
if err := json.Unmarshal(data, &parsed); err != nil {
|
||||
t.Errorf("%s is not valid JSON: %v", name, err)
|
||||
continue
|
||||
}
|
||||
if parsed["name"] == nil {
|
||||
t.Errorf("%s missing 'name' field", name)
|
||||
}
|
||||
if parsed["process"] == nil {
|
||||
t.Errorf("%s missing 'process' field", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Integration tests (require test.Prepare via TestMain) ---
|
||||
|
||||
func TestListHandler_All(t *testing.T) {
|
||||
proc := &process.Process{Args: []interface{}{}}
|
||||
result := ListHandler(proc)
|
||||
m, ok := result.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected map, got %T", result)
|
||||
}
|
||||
if errMsg, has := m["error"]; has {
|
||||
t.Fatalf("ListHandler returned error: %v", errMsg)
|
||||
}
|
||||
agents, ok := m["agents"]
|
||||
if !ok {
|
||||
t.Fatal("ListHandler result missing 'agents' key")
|
||||
}
|
||||
agentList, ok := agents.([]agentInfo)
|
||||
if !ok {
|
||||
t.Fatalf("agents field is %T, expected []agentInfo", agents)
|
||||
}
|
||||
if len(agentList) == 0 {
|
||||
t.Error("expected at least one agent in yao-dev-app")
|
||||
}
|
||||
for _, a := range agentList {
|
||||
if a.ID == "" {
|
||||
t.Error("agent ID should not be empty")
|
||||
}
|
||||
if !contains(a.ID, ".") {
|
||||
t.Errorf("agent ID %q should use dot notation", a.ID)
|
||||
}
|
||||
}
|
||||
t.Logf("ListHandler returned %d agents", len(agentList))
|
||||
}
|
||||
|
||||
func TestListHandler_Namespace(t *testing.T) {
|
||||
proc := &process.Process{Args: []interface{}{"yaobots"}}
|
||||
result := ListHandler(proc)
|
||||
m := result.(map[string]interface{})
|
||||
if errMsg, has := m["error"]; has {
|
||||
t.Fatalf("ListHandler returned error: %v", errMsg)
|
||||
}
|
||||
agentList := m["agents"].([]agentInfo)
|
||||
for _, a := range agentList {
|
||||
if !hasPrefix(a.ID, "yaobots.") {
|
||||
t.Errorf("agent %q should be in yaobots namespace", a.ID)
|
||||
}
|
||||
}
|
||||
t.Logf("namespace 'yaobots': %d agents", len(agentList))
|
||||
}
|
||||
|
||||
func TestListHandler_NonexistentNamespace(t *testing.T) {
|
||||
proc := &process.Process{Args: []interface{}{"nonexistent_ns_xyz"}}
|
||||
result := ListHandler(proc)
|
||||
m := result.(map[string]interface{})
|
||||
agentList := m["agents"].([]agentInfo)
|
||||
if len(agentList) != 0 {
|
||||
t.Errorf("expected 0 agents for nonexistent namespace, got %d", len(agentList))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListHandler_SkipsYaoInternal(t *testing.T) {
|
||||
proc := &process.Process{Args: []interface{}{}}
|
||||
result := ListHandler(proc)
|
||||
m := result.(map[string]interface{})
|
||||
agentList := m["agents"].([]agentInfo)
|
||||
for _, a := range agentList {
|
||||
if hasPrefix(a.ID, "__yao.") {
|
||||
t.Errorf("internal agent %q should be filtered out", a.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectorsHandler_NoProvider(t *testing.T) {
|
||||
saved := llmprovider.Global
|
||||
llmprovider.Global = nil
|
||||
defer func() { llmprovider.Global = saved }()
|
||||
|
||||
proc := &process.Process{Args: []interface{}{}}
|
||||
result := ConnectorsHandler(proc)
|
||||
m, ok := result.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected map, got %T", result)
|
||||
}
|
||||
errMsg, has := m["error"]
|
||||
if !has {
|
||||
t.Fatal("expected error when llmprovider.Global is nil")
|
||||
}
|
||||
if !contains(errMsg.(string), "not initialized") {
|
||||
t.Errorf("error = %q, want substring 'not initialized'", errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectorsHandler_WithProvider(t *testing.T) {
|
||||
if err := setting.Init(); err != nil {
|
||||
t.Skipf("setting.Init failed: %v", err)
|
||||
}
|
||||
if err := llmprovider.Init(); err != nil {
|
||||
t.Skipf("llmprovider.Init failed (may need full env): %v", err)
|
||||
}
|
||||
if llmprovider.Global == nil {
|
||||
t.Skip("llmprovider.Global is nil after Init")
|
||||
}
|
||||
|
||||
proc := &process.Process{Args: []interface{}{}}
|
||||
result := ConnectorsHandler(proc)
|
||||
m, ok := result.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected map, got %T", result)
|
||||
}
|
||||
if errMsg, has := m["error"]; has {
|
||||
t.Fatalf("ConnectorsHandler returned error: %v", errMsg)
|
||||
}
|
||||
t.Logf("ConnectorsHandler returned %d roles", len(m))
|
||||
}
|
||||
|
||||
func TestDeployHandler_MissingID(t *testing.T) {
|
||||
proc := &process.Process{Args: []interface{}{""}}
|
||||
result := DeployHandler(proc)
|
||||
m := result.(map[string]interface{})
|
||||
if _, has := m["error"]; !has {
|
||||
t.Error("expected error for empty id")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeployHandler_WrongNamespace(t *testing.T) {
|
||||
proc := &process.Process{Args: []interface{}{"yao.slides"}}
|
||||
result := DeployHandler(proc)
|
||||
m := result.(map[string]interface{})
|
||||
if m["status"] != "error" {
|
||||
t.Errorf("expected status 'error' for non-smith namespace, got %v", m["status"])
|
||||
}
|
||||
msg, _ := m["message"].(string)
|
||||
if !contains(msg, "smith") {
|
||||
t.Errorf("error message should mention 'smith', got %q", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeployHandler_InvalidID(t *testing.T) {
|
||||
cases := []string{"smith/bad", "a..b", "onlyname"}
|
||||
for _, id := range cases {
|
||||
proc := &process.Process{Args: []interface{}{id}}
|
||||
result := DeployHandler(proc)
|
||||
m := result.(map[string]interface{})
|
||||
if _, has := m["error"]; !has {
|
||||
t.Errorf("DeployHandler(%q) expected error", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadHandler_MissingID(t *testing.T) {
|
||||
proc := &process.Process{Args: []interface{}{""}}
|
||||
result := DownloadHandler(proc)
|
||||
m := result.(map[string]interface{})
|
||||
if _, has := m["error"]; !has {
|
||||
t.Error("expected error for empty id")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadHandler_InvalidID(t *testing.T) {
|
||||
cases := []string{"no/slash", "a..b", ""}
|
||||
for _, id := range cases {
|
||||
proc := &process.Process{Args: []interface{}{id}}
|
||||
result := DownloadHandler(proc)
|
||||
m := result.(map[string]interface{})
|
||||
if _, has := m["error"]; !has {
|
||||
t.Errorf("DownloadHandler(%q) expected error", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadHandler_MissingWorkspace(t *testing.T) {
|
||||
proc := &process.Process{
|
||||
Args: []interface{}{"yao.slides"},
|
||||
Context: context.Background(),
|
||||
}
|
||||
result := DownloadHandler(proc)
|
||||
m := result.(map[string]interface{})
|
||||
errMsg, has := m["error"]
|
||||
if !has {
|
||||
t.Fatal("expected error when workspace_id is missing")
|
||||
}
|
||||
if !contains(errMsg.(string), "workspace_id") {
|
||||
t.Errorf("error = %q, want substring 'workspace_id'", errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeployHandler_MissingWorkspace(t *testing.T) {
|
||||
proc := &process.Process{
|
||||
Args: []interface{}{"smith.test"},
|
||||
Context: context.Background(),
|
||||
}
|
||||
result := DeployHandler(proc)
|
||||
m := result.(map[string]interface{})
|
||||
errMsg, has := m["error"]
|
||||
if !has {
|
||||
t.Fatal("expected error when workspace_id is missing")
|
||||
}
|
||||
if !contains(errMsg.(string), "workspace_id") {
|
||||
t.Errorf("error = %q, want substring 'workspace_id'", errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && searchSubstring(s, substr)
|
||||
}
|
||||
|
||||
func searchSubstring(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasPrefix(s, prefix string) bool {
|
||||
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
|
||||
}
|
||||
63
tools/agent/connectors.go
Normal file
63
tools/agent/connectors.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/yaoapp/gou/connector"
|
||||
"github.com/yaoapp/gou/process"
|
||||
"github.com/yaoapp/yao/llmprovider"
|
||||
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
||||
)
|
||||
|
||||
// ConnectorsHandler handles the agent_connectors tool.
|
||||
// No input args. Returns the current user's LLM connector matrix without keys.
|
||||
func ConnectorsHandler(proc *process.Process) interface{} {
|
||||
authInfo := authorized.ProcessAuthInfo(proc)
|
||||
|
||||
if llmprovider.Global == nil {
|
||||
return map[string]interface{}{"error": "llmprovider not initialized"}
|
||||
}
|
||||
|
||||
var roles map[string]llmprovider.RoleTarget
|
||||
var err error
|
||||
|
||||
if authInfo != nil && authInfo.UserID != "" {
|
||||
roles, err = llmprovider.Global.ListRolesByUser(authInfo.UserID)
|
||||
} else {
|
||||
roles, err = llmprovider.Global.ListRoles()
|
||||
}
|
||||
if err != nil {
|
||||
return map[string]interface{}{"error": fmt.Sprintf("failed to list roles: %s", err.Error())}
|
||||
}
|
||||
|
||||
result := make(map[string]interface{}, len(roles))
|
||||
for role, target := range roles {
|
||||
connID := target.Provider
|
||||
info := map[string]interface{}{
|
||||
"id": connID,
|
||||
"model": target.Model,
|
||||
}
|
||||
|
||||
conn, exists := connector.Connectors[connID]
|
||||
if exists {
|
||||
setting := conn.Setting()
|
||||
meta := conn.GetMetaInfo()
|
||||
if meta.Label != "" {
|
||||
info["name"] = meta.Label
|
||||
}
|
||||
if model, ok := setting["model"]; ok && info["model"] == "" {
|
||||
info["model"] = model
|
||||
}
|
||||
if caps, ok := setting["capabilities"]; ok {
|
||||
info["capabilities"] = sanitizeCapabilities(caps)
|
||||
}
|
||||
if t := settingStr(setting, "auth_mode"); t != "" {
|
||||
info["type"] = "openai"
|
||||
}
|
||||
}
|
||||
|
||||
result[role] = info
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
10
tools/agent/connectors_schema.json
Normal file
10
tools/agent/connectors_schema.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "agent_connectors",
|
||||
"description": "Get the current user's LLM connector matrix. Returns connector metadata for each role (default, heavy, light, vision, etc.) without API keys. Use this to understand available models and their capabilities.",
|
||||
"process": "tools.agent_connectors",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"x-process-args": []
|
||||
}
|
||||
73
tools/agent/deploy.go
Normal file
73
tools/agent/deploy.go
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/yaoapp/gou/process"
|
||||
"github.com/yaoapp/kun/log"
|
||||
"github.com/yaoapp/yao/agent/caller"
|
||||
"github.com/yaoapp/yao/config"
|
||||
)
|
||||
|
||||
// DeployHandler handles the agent_deploy tool.
|
||||
// Args[0]: id (string, dot notation e.g. "smith.weather")
|
||||
// Args[1]: message (string, optional deploy message)
|
||||
func DeployHandler(proc *process.Process) interface{} {
|
||||
id := proc.ArgsString(0)
|
||||
if id == "" {
|
||||
return map[string]interface{}{"error": "id is required (e.g. 'smith.weather')"}
|
||||
}
|
||||
if err := validateID(id); err != nil {
|
||||
return map[string]interface{}{"error": err.Error()}
|
||||
}
|
||||
|
||||
parts := strings.SplitN(id, ".", 2)
|
||||
if len(parts) != 2 || parts[0] != allowedDeployNamespace {
|
||||
return map[string]interface{}{
|
||||
"status": "error",
|
||||
"message": fmt.Sprintf("deploy restricted to namespace '%s'", allowedDeployNamespace),
|
||||
}
|
||||
}
|
||||
|
||||
wsFS, err := resolveWorkspaceFS(proc)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"error": err.Error()}
|
||||
}
|
||||
|
||||
relPath := idToPath(id)
|
||||
appRoot := config.Conf.Root
|
||||
srcPath := filepath.Join("agent-smith-dev", "assistants", relPath)
|
||||
dstURI := "local:///" + filepath.Join(appRoot, "assistants", relPath)
|
||||
|
||||
result, copyErr := wsFS.Copy(srcPath, dstURI)
|
||||
if copyErr != nil {
|
||||
return map[string]interface{}{"error": fmt.Sprintf("deploy failed: %s", copyErr.Error())}
|
||||
}
|
||||
|
||||
files := 0
|
||||
if result != nil {
|
||||
files = result.FilesSynced
|
||||
}
|
||||
|
||||
msg := ""
|
||||
if len(proc.Args) > 1 {
|
||||
msg = proc.ArgsString(1)
|
||||
}
|
||||
if msg != "" {
|
||||
log.Info("[agent_deploy] %s: %s (%d files)", id, msg, files)
|
||||
}
|
||||
|
||||
if caller.AssistantReloadFunc != nil {
|
||||
if err := caller.AssistantReloadFunc(id); err != nil {
|
||||
log.Warn("[agent_deploy] reload %s: %s (files deployed, restart to apply)", id, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"status": "ok",
|
||||
"path": filepath.Join("assistants", relPath),
|
||||
"synced_files": files,
|
||||
}
|
||||
}
|
||||
20
tools/agent/deploy_schema.json
Normal file
20
tools/agent/deploy_schema.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "agent_deploy",
|
||||
"description": "Deploy agent source code from the sandbox development directory to the host. Restricted to the 'smith' namespace only.",
|
||||
"process": "tools.agent_deploy",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Agent ID in dot notation (e.g. 'smith.weather'). Must use 'smith' namespace."
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Optional deploy message for logging purposes."
|
||||
}
|
||||
},
|
||||
"required": ["id"]
|
||||
},
|
||||
"x-process-args": ["$args.id", "$args.message"]
|
||||
}
|
||||
47
tools/agent/download.go
Normal file
47
tools/agent/download.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/yaoapp/gou/process"
|
||||
"github.com/yaoapp/yao/config"
|
||||
)
|
||||
|
||||
// DownloadHandler handles the agent_download tool.
|
||||
// Args[0]: id (string, dot notation e.g. "yao.slides")
|
||||
func DownloadHandler(proc *process.Process) interface{} {
|
||||
id := proc.ArgsString(0)
|
||||
if id == "" {
|
||||
return map[string]interface{}{"error": "id is required (e.g. 'yao.slides')"}
|
||||
}
|
||||
if err := validateID(id); err != nil {
|
||||
return map[string]interface{}{"error": err.Error()}
|
||||
}
|
||||
|
||||
wsFS, err := resolveWorkspaceFS(proc)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"error": err.Error()}
|
||||
}
|
||||
|
||||
relPath := idToPath(id)
|
||||
appRoot := config.Conf.Root
|
||||
srcURI := "local:///" + filepath.Join(appRoot, "assistants", relPath)
|
||||
dstPath := filepath.Join("agent-smith-dev", "assistants", relPath)
|
||||
|
||||
result, copyErr := wsFS.Copy(srcURI, dstPath)
|
||||
if copyErr != nil {
|
||||
return map[string]interface{}{"error": fmt.Sprintf("download failed: %s", copyErr.Error())}
|
||||
}
|
||||
|
||||
files := 0
|
||||
if result != nil {
|
||||
files = result.FilesSynced
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"status": "ok",
|
||||
"path": dstPath,
|
||||
"files": files,
|
||||
}
|
||||
}
|
||||
16
tools/agent/download_schema.json
Normal file
16
tools/agent/download_schema.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "agent_download",
|
||||
"description": "Download agent source code from the host into the sandbox development directory. Use this to study existing agents as reference. Any agent can be downloaded (read-only reference).",
|
||||
"process": "tools.agent_download",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Agent ID in dot notation (e.g. 'yao.slides', 'smith.weather')"
|
||||
}
|
||||
},
|
||||
"required": ["id"]
|
||||
},
|
||||
"x-process-args": ["$args.id"]
|
||||
}
|
||||
83
tools/agent/list.go
Normal file
83
tools/agent/list.go
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
goufs "github.com/yaoapp/gou/fs"
|
||||
"github.com/yaoapp/gou/process"
|
||||
)
|
||||
|
||||
// ListHandler handles the agent_list tool.
|
||||
// Args[0]: namespace (string, optional)
|
||||
func ListHandler(proc *process.Process) interface{} {
|
||||
namespace := ""
|
||||
if len(proc.Args) > 0 {
|
||||
namespace = proc.ArgsString(0)
|
||||
}
|
||||
|
||||
app, err := goufs.Get("app")
|
||||
if err != nil {
|
||||
return map[string]interface{}{"error": fmt.Sprintf("app filesystem: %s", err.Error())}
|
||||
}
|
||||
|
||||
root := "/assistants"
|
||||
exists, _ := app.Exists(root)
|
||||
if !exists {
|
||||
return map[string]interface{}{"agents": []agentInfo{}}
|
||||
}
|
||||
|
||||
nsDirs, err := app.ReadDir(root, false)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"error": fmt.Sprintf("read assistants dir: %s", err.Error())}
|
||||
}
|
||||
|
||||
agents := make([]agentInfo, 0)
|
||||
for _, nsDir := range nsDirs {
|
||||
nsName := filepath.Base(nsDir)
|
||||
if namespace != "" && nsName != namespace {
|
||||
continue
|
||||
}
|
||||
|
||||
agentDirs, err := app.ReadDir(nsDir, false)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, agentDir := range agentDirs {
|
||||
pkgFile := filepath.Join(agentDir, "package.yao")
|
||||
pkgExists, _ := app.Exists(pkgFile)
|
||||
if !pkgExists {
|
||||
continue
|
||||
}
|
||||
|
||||
data, err := app.ReadFile(pkgFile)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var pkg packageDSL
|
||||
if err := json.Unmarshal(data, &pkg); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
agentName := filepath.Base(agentDir)
|
||||
id := nsName + "." + agentName
|
||||
|
||||
if strings.HasPrefix(id, "__yao.") {
|
||||
continue
|
||||
}
|
||||
|
||||
agents = append(agents, agentInfo{
|
||||
ID: id,
|
||||
Name: pkg.Name,
|
||||
Description: pkg.Description,
|
||||
Capabilities: pkg.Capabilities,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{"agents": agents}
|
||||
}
|
||||
15
tools/agent/list_schema.json
Normal file
15
tools/agent/list_schema.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "agent_list",
|
||||
"description": "List available agents on the host. Returns agent ID, name, description, and capabilities. Optionally filter by namespace.",
|
||||
"process": "tools.agent_list",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"namespace": {
|
||||
"type": "string",
|
||||
"description": "Optional namespace filter (e.g. 'yao', 'smith'). If omitted, lists all agents."
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-process-args": ["$args.namespace"]
|
||||
}
|
||||
11
tools/mcps/agent.json
Normal file
11
tools/mcps/agent.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "yao-agent",
|
||||
"transport": "process",
|
||||
"description": "Agent management tools for listing, downloading, deploying agents, and querying connector matrix",
|
||||
"tools": {
|
||||
"agent_list": "tools.agent_list",
|
||||
"agent_download": "tools.agent_download",
|
||||
"agent_deploy": "tools.agent_deploy",
|
||||
"agent_connectors": "tools.agent_connectors"
|
||||
}
|
||||
}
|
||||
|
|
@ -76,8 +76,12 @@ You have access to Yao system tools via the `tai` command in bash.
|
|||
| `doc_list` | yao-doc | Search/list available process documentation |
|
||||
| `doc_inspect` | yao-doc | Get detailed docs for a specific process |
|
||||
| `doc_validate` | yao-doc | Validate a process name and get suggestions |
|
||||
| `image_read` | yao-image | Read and analyze images using a vision model |
|
||||
| `image_generate` | yao-image | Generate images from text prompts |
|
||||
| `image_providers` | yao-image | List available image generation or vision providers |
|
||||
| `image_read` | yao-image | Read and analyze images using a vision model |
|
||||
| `image_generate` | yao-image | Generate images from text prompts |
|
||||
| `image_providers` | yao-image | List available image generation or vision providers |
|
||||
| `agent_list` | yao-agent | List available agents on the host |
|
||||
| `agent_download` | yao-agent | Download agent source code for reference |
|
||||
| `agent_deploy` | yao-agent | Deploy agent code to host (smith namespace only) |
|
||||
| `agent_connectors` | yao-agent | Get LLM connector matrix (no keys) |
|
||||
|
||||
The system skills (`yao-web`, `yao-process`, `yao-doc`, `yao-image`) in `$HOME/.claude/skills/` are **auto-discovered** — they contain detailed parameter docs and workflow guidance. You do not need to manually read them; they are loaded automatically when your task matches their description.
|
||||
The system skills (`yao-web`, `yao-process`, `yao-doc`, `yao-image`, `yao-agent`) in `$HOME/.claude/skills/` are **auto-discovered** — they contain detailed parameter docs and workflow guidance. You do not need to manually read them; they are loaded automatically when your task matches their description.
|
||||
|
|
|
|||
65
tools/skills/yao-agent/SKILL.md
Normal file
65
tools/skills/yao-agent/SKILL.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
---
|
||||
name: yao-agent
|
||||
description: Agent management expert. ALWAYS invoke this skill when you need to list available agents, download agent source code for reference, deploy agent code to the host, or query the LLM connector matrix. Do not guess agent structures — use this skill first.
|
||||
---
|
||||
|
||||
# Agent Tools
|
||||
|
||||
Four tools for managing agents on the host, called via bash.
|
||||
|
||||
## agent_list
|
||||
|
||||
List available agents. Returns ID, name, description, and capabilities for each agent.
|
||||
|
||||
```bash
|
||||
tai tool agent_list '{}'
|
||||
tai tool agent_list '{"namespace": "smith"}'
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-------------|--------|----------|----------------------------------------------------------|
|
||||
| `namespace` | string | no | Filter by namespace (e.g. `yao`, `smith`). Omit for all. |
|
||||
|
||||
## agent_download
|
||||
|
||||
Download agent source code from the host into `agent-smith-dev/assistants/` for reference. Any agent across all namespaces can be downloaded (read-only).
|
||||
|
||||
```bash
|
||||
tai tool agent_download '{"id": "yao.slides"}'
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|--------|----------|----------------------------------------------------|
|
||||
| `id` | string | yes | Agent ID in dot notation (e.g. `yao.slides`) |
|
||||
|
||||
## agent_deploy
|
||||
|
||||
Deploy agent source code from the sandbox development directory to the host. **Restricted to the `smith` namespace only** — attempts to deploy to other namespaces will be rejected.
|
||||
|
||||
```bash
|
||||
tai tool agent_deploy '{"id": "smith.weather"}'
|
||||
tai tool agent_deploy '{"id": "smith.weather", "message": "add SUI page"}'
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|--------|----------|--------------------------------------------------------|
|
||||
| `id` | string | yes | Agent ID in dot notation. Must use `smith` namespace. |
|
||||
| `message` | string | no | Optional deploy message for logging. |
|
||||
|
||||
## agent_connectors
|
||||
|
||||
Get the current user's LLM connector matrix. Returns metadata for each role (default, heavy, light, vision, etc.) **without API keys**. Use this to understand which models are available and their capabilities.
|
||||
|
||||
```bash
|
||||
tai tool agent_connectors '{}'
|
||||
```
|
||||
|
||||
No parameters required.
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Use `agent_list` to discover agents before downloading
|
||||
- Downloaded code lands in `agent-smith-dev/assistants/<namespace>/<name>/`
|
||||
- Deploy is restricted to the `smith` namespace for safety
|
||||
- Connector data never includes API keys, secrets, or tokens
|
||||
- All output is JSON
|
||||
|
|
@ -12,6 +12,7 @@ func TestSkillsFS_ContainsAllSkills(t *testing.T) {
|
|||
"skills/yao-process/SKILL.md": false,
|
||||
"skills/yao-doc/SKILL.md": false,
|
||||
"skills/yao-image/SKILL.md": false,
|
||||
"skills/yao-agent/SKILL.md": false,
|
||||
}
|
||||
|
||||
err := fs.WalkDir(SkillsFS, "skills", func(path string, d fs.DirEntry, err error) error {
|
||||
|
|
@ -43,6 +44,7 @@ func TestSkillsFS_FrontmatterFields(t *testing.T) {
|
|||
{"skills/yao-process/SKILL.md", "yao-process"},
|
||||
{"skills/yao-doc/SKILL.md", "yao-doc"},
|
||||
{"skills/yao-image/SKILL.md", "yao-image"},
|
||||
{"skills/yao-agent/SKILL.md", "yao-agent"},
|
||||
}
|
||||
|
||||
for _, s := range skills {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
mcpTypes "github.com/yaoapp/gou/mcp/types"
|
||||
"github.com/yaoapp/gou/process"
|
||||
"github.com/yaoapp/kun/log"
|
||||
"github.com/yaoapp/yao/tools/agent"
|
||||
"github.com/yaoapp/yao/tools/docs"
|
||||
"github.com/yaoapp/yao/tools/image"
|
||||
"github.com/yaoapp/yao/tools/proc"
|
||||
|
|
@ -27,18 +28,25 @@ var mcpDocDSL []byte
|
|||
//go:embed mcps/image.json
|
||||
var mcpImageDSL []byte
|
||||
|
||||
//go:embed mcps/agent.json
|
||||
var mcpAgentDSL []byte
|
||||
|
||||
func init() {
|
||||
process.RegisterGroup("tools", map[string]process.Handler{
|
||||
"web_search": websearch.Handler,
|
||||
"web_fetch": webfetch.Handler,
|
||||
"process_call": proc.Handler,
|
||||
"process_allowed": proc.AllowedHandler,
|
||||
"doc_list": docs.ListHandler,
|
||||
"doc_inspect": docs.InspectHandler,
|
||||
"doc_validate": docs.ValidateHandler,
|
||||
"image_read": image.ReadHandler,
|
||||
"image_generate": image.GenerateHandler,
|
||||
"image_providers": image.ProvidersHandler,
|
||||
"web_search": websearch.Handler,
|
||||
"web_fetch": webfetch.Handler,
|
||||
"process_call": proc.Handler,
|
||||
"process_allowed": proc.AllowedHandler,
|
||||
"doc_list": docs.ListHandler,
|
||||
"doc_inspect": docs.InspectHandler,
|
||||
"doc_validate": docs.ValidateHandler,
|
||||
"image_read": image.ReadHandler,
|
||||
"image_generate": image.GenerateHandler,
|
||||
"image_providers": image.ProvidersHandler,
|
||||
"agent_list": agent.ListHandler,
|
||||
"agent_download": agent.DownloadHandler,
|
||||
"agent_deploy": agent.DeployHandler,
|
||||
"agent_connectors": agent.ConnectorsHandler,
|
||||
})
|
||||
|
||||
registerMCPServer(mcpWebDSL, "yao-web",
|
||||
|
|
@ -49,6 +57,9 @@ func init() {
|
|||
docs.ListSchemaJSON, docs.InspectSchemaJSON, docs.ValidateSchemaJSON)
|
||||
registerMCPServer(mcpImageDSL, "yao-image",
|
||||
image.ReadSchemaJSON, image.GenerateSchemaJSON, image.ProvidersSchemaJSON)
|
||||
registerMCPServer(mcpAgentDSL, "yao-agent",
|
||||
agent.ListSchemaJSON, agent.DownloadSchemaJSON, agent.DeploySchemaJSON,
|
||||
agent.ConnectorsSchemaJSON)
|
||||
}
|
||||
|
||||
func registerMCPServer(dsl []byte, id string, schemas ...[]byte) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue