完善 MCP 工具

This commit is contained in:
2026-06-11 08:21:09 +08:00
parent c0f15050d0
commit 1415d3b43b
20 changed files with 3722 additions and 61 deletions
+434
View File
@@ -0,0 +1,434 @@
package mcp
import (
"context"
"time"
"github.com/google/uuid"
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
)
// ===== ACP 桥接抽象 =====
//
// acp 包 import 了 mcp(supervisor 签发 system token),mcp 不能反向 import acp。
// 这里定义 acp-free 的桥接接口与 DTO,由 internal/acp/mcp_bridge.go 实现适配
// (acp → mcp 的既有依赖边,不新增循环)。
//
// 范围边界(spec 决策):agent kind 的增删改(含加密 env)与权限 approve/deny
// 不经 MCP 暴露——前者是秘密载体,后者的设计初衷是人工把关。
// ACPAgentKind 是 agent kind 的 MCP 投影(不含 binary/args/env 等部署细节)。
type ACPAgentKind struct {
ID uuid.UUID
Name string
DisplayName string
Description string
Enabled bool
ToolAllowlist []string
}
// ACPSession 是 ACP 会话的 MCP 投影。
type ACPSession struct {
ID uuid.UUID
WorkspaceID uuid.UUID
ProjectID uuid.UUID
AgentKindID uuid.UUID
UserID uuid.UUID
IssueID *uuid.UUID
RequirementID *uuid.UUID
Branch string
IsMainWorktree bool
Status string
LastError *string
StartedAt time.Time
EndedAt *time.Time
}
// ACPPermission 是待审权限请求的 MCP 投影(只读,不含原始 toolCall/options JSON)。
type ACPPermission struct {
ID uuid.UUID
SessionID uuid.UUID
AgentRequestID string
ToolName string
Status string
CreatedAt time.Time
}
// ACPCreateSessionInput 与 acp.CreateSessionInput 字段一一对应。
type ACPCreateSessionInput struct {
WorkspaceID uuid.UUID
AgentKindID uuid.UUID
Branch *string
IssueNumber *int
RequirementNumber *int
InitialPrompt *string
}
// ACPSessionFilter 与 acp.SessionListFilter 字段一一对应。
type ACPSessionFilter struct {
All bool
RequirementID *uuid.UUID
}
// ACPBridge 是 mcp 调用 acp 服务的桥接接口。
type ACPBridge interface {
ListAgentKinds(ctx context.Context, userID uuid.UUID, isAdmin bool) ([]ACPAgentKind, error)
GetAgentKind(ctx context.Context, userID uuid.UUID, isAdmin bool, id uuid.UUID) (*ACPAgentKind, error)
CreateSession(ctx context.Context, userID uuid.UUID, isAdmin bool, in ACPCreateSessionInput) (*ACPSession, error)
GetSession(ctx context.Context, userID uuid.UUID, isAdmin bool, id uuid.UUID) (*ACPSession, error)
ListSessions(ctx context.Context, userID uuid.UUID, isAdmin bool, f ACPSessionFilter) ([]ACPSession, error)
TerminateSession(ctx context.Context, userID uuid.UUID, isAdmin bool, id uuid.UUID) error
ListPendingPermissions(ctx context.Context, userID uuid.UUID, isAdmin bool, sessionID uuid.UUID) ([]ACPPermission, error)
}
// registerACPTools 注册 ACP 相关 MCP 工具。
func registerACPTools(srv *mcpsdk.Server, deps ServerDeps) {
registerListAgentKinds(srv, deps)
registerGetAgentKind(srv, deps)
registerCreateACPSession(srv, deps)
registerGetACPSession(srv, deps)
registerListACPSessions(srv, deps)
registerTerminateACPSession(srv, deps)
registerListPendingPermissions(srv, deps)
}
// ===== DTO =====
type agentKindDetail struct {
ID string `json:"id"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
Description string `json:"description,omitempty"`
Enabled bool `json:"enabled"`
ToolAllowlist []string `json:"tool_allowlist,omitempty"`
}
func agentKindDetailFrom(k ACPAgentKind) agentKindDetail {
return agentKindDetail{
ID: k.ID.String(), Name: k.Name, DisplayName: k.DisplayName,
Description: k.Description, Enabled: k.Enabled, ToolAllowlist: k.ToolAllowlist,
}
}
type acpSessionDetail struct {
ID string `json:"id"`
WorkspaceID string `json:"workspace_id"`
ProjectID string `json:"project_id"`
AgentKindID string `json:"agent_kind_id"`
UserID string `json:"user_id"`
IssueID string `json:"issue_id,omitempty"`
RequirementID string `json:"requirement_id,omitempty"`
Branch string `json:"branch,omitempty"`
IsMainWorktree bool `json:"is_main_worktree"`
Status string `json:"status"`
LastError string `json:"last_error,omitempty"`
StartedAt string `json:"started_at"`
EndedAt string `json:"ended_at,omitempty"`
}
func acpSessionDetailFrom(s *ACPSession) acpSessionDetail {
d := acpSessionDetail{
ID: s.ID.String(), WorkspaceID: s.WorkspaceID.String(),
ProjectID: s.ProjectID.String(), AgentKindID: s.AgentKindID.String(),
UserID: s.UserID.String(), Branch: s.Branch,
IsMainWorktree: s.IsMainWorktree, Status: s.Status,
StartedAt: s.StartedAt.Format("2006-01-02T15:04:05Z07:00"),
}
if s.IssueID != nil {
d.IssueID = s.IssueID.String()
}
if s.RequirementID != nil {
d.RequirementID = s.RequirementID.String()
}
if s.LastError != nil {
d.LastError = *s.LastError
}
if s.EndedAt != nil {
d.EndedAt = s.EndedAt.Format("2006-01-02T15:04:05Z07:00")
}
return d
}
// checkNotSystemToken 拒绝 system token(agent 会话签发)调用:用于
// create_acp_session,防止 agent 在会话内套娃创建嵌套会话。
func checkNotSystemToken(ctx context.Context) error {
sess, ok := FromContext(ctx)
if !ok {
return errs.New(errs.CodeInternal, "mcp: AuthSession missing")
}
if sess.Issuer == IssuerSystem {
return errs.New(errs.CodeForbidden,
"system tokens (agent sessions) cannot create nested ACP sessions")
}
return nil
}
// scopedACPSession 取 session 并做项目 scope 校验。
func scopedACPSession(ctx context.Context, deps ServerDeps, userID uuid.UUID, isAdmin bool, sessionID string) (*ACPSession, error) {
sid, err := uuid.Parse(sessionID)
if err != nil {
return nil, errs.Wrap(err, errs.CodeInvalidInput, "session_id parse")
}
s, err := deps.ACP.GetSession(ctx, userID, isAdmin, sid)
if err != nil {
return nil, err
}
if err := CheckProjectFromCtx(ctx, s.ProjectID); err != nil {
return nil, err
}
return s, nil
}
// ===== list_agent_kinds / get_agent_kind =====
type listAgentKindsOut struct {
AgentKinds []agentKindDetail `json:"agent_kinds"`
}
func registerListAgentKinds(srv *mcpsdk.Server, deps ServerDeps) {
mcpsdk.AddTool(srv,
&mcpsdk.Tool{Name: "list_agent_kinds", Description: "List ACP agent kinds available to the caller."},
func(ctx context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, listAgentKindsOut, error) {
if err := CheckToolFromCtx(ctx, "list_agent_kinds"); err != nil {
return nil, listAgentKindsOut{}, err
}
c, err := deps.Caller.Resolve(ctx)
if err != nil {
return nil, listAgentKindsOut{}, err
}
ks, err := deps.ACP.ListAgentKinds(ctx, c.UserID, c.IsAdmin)
if err != nil {
return nil, listAgentKindsOut{}, err
}
out := listAgentKindsOut{AgentKinds: make([]agentKindDetail, 0, len(ks))}
for _, k := range ks {
out.AgentKinds = append(out.AgentKinds, agentKindDetailFrom(k))
}
return nil, out, nil
})
}
type getAgentKindIn struct {
AgentKindID string `json:"agent_kind_id" jsonschema:"agent kind UUID"`
}
func registerGetAgentKind(srv *mcpsdk.Server, deps ServerDeps) {
mcpsdk.AddTool(srv,
&mcpsdk.Tool{Name: "get_agent_kind", Description: "Get a single ACP agent kind by ID."},
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getAgentKindIn) (*mcpsdk.CallToolResult, agentKindDetail, error) {
if err := CheckToolFromCtx(ctx, "get_agent_kind"); err != nil {
return nil, agentKindDetail{}, err
}
c, err := deps.Caller.Resolve(ctx)
if err != nil {
return nil, agentKindDetail{}, err
}
kid, err := uuid.Parse(in.AgentKindID)
if err != nil {
return nil, agentKindDetail{}, errs.Wrap(err, errs.CodeInvalidInput, "agent_kind_id parse")
}
k, err := deps.ACP.GetAgentKind(ctx, c.UserID, c.IsAdmin, kid)
if err != nil {
return nil, agentKindDetail{}, err
}
return nil, agentKindDetailFrom(*k), nil
})
}
// ===== create_acp_session =====
type createACPSessionIn struct {
WorkspaceID string `json:"workspace_id" jsonschema:"workspace UUID"`
AgentKindID string `json:"agent_kind_id" jsonschema:"agent kind UUID"`
Branch string `json:"branch,omitempty" jsonschema:"work on this branch (mutually exclusive with issue/requirement number)"`
IssueNumber int `json:"issue_number,omitempty" jsonschema:"bind to an issue"`
RequirementNumber int `json:"requirement_number,omitempty" jsonschema:"bind to a requirement"`
InitialPrompt string `json:"initial_prompt,omitempty" jsonschema:"prompt sent to the agent after startup"`
}
type acpSessionCreateOut struct {
OK bool `json:"ok"`
Session acpSessionDetail `json:"session"`
}
func registerCreateACPSession(srv *mcpsdk.Server, deps ServerDeps) {
mcpsdk.AddTool(srv,
&mcpsdk.Tool{Name: "create_acp_session", Description: "Start an ACP agent session on a workspace."},
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in createACPSessionIn) (*mcpsdk.CallToolResult, acpSessionCreateOut, error) {
if err := CheckToolFromCtx(ctx, "create_acp_session"); err != nil {
return nil, acpSessionCreateOut{}, err
}
if err := checkNotSystemToken(ctx); err != nil {
return nil, acpSessionCreateOut{}, err
}
c, err := deps.Caller.Resolve(ctx)
if err != nil {
return nil, acpSessionCreateOut{}, err
}
w, _, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID)
if err != nil {
return nil, acpSessionCreateOut{}, err
}
kid, err := uuid.Parse(in.AgentKindID)
if err != nil {
return nil, acpSessionCreateOut{}, errs.Wrap(err, errs.CodeInvalidInput, "agent_kind_id parse")
}
input := ACPCreateSessionInput{WorkspaceID: w.ID, AgentKindID: kid}
if in.Branch != "" {
input.Branch = &in.Branch
}
if in.IssueNumber > 0 {
n := in.IssueNumber
input.IssueNumber = &n
}
if in.RequirementNumber > 0 {
n := in.RequirementNumber
input.RequirementNumber = &n
}
if in.InitialPrompt != "" {
input.InitialPrompt = &in.InitialPrompt
}
s, err := deps.ACP.CreateSession(ctx, c.UserID, c.IsAdmin, input)
if err != nil {
return nil, acpSessionCreateOut{}, err
}
return nil, acpSessionCreateOut{OK: true, Session: acpSessionDetailFrom(s)}, nil
})
}
// ===== get/list/terminate_acp_session =====
type getACPSessionIn struct {
SessionID string `json:"session_id" jsonschema:"ACP session UUID"`
}
func registerGetACPSession(srv *mcpsdk.Server, deps ServerDeps) {
mcpsdk.AddTool(srv,
&mcpsdk.Tool{Name: "get_acp_session", Description: "Get a single ACP session by ID."},
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getACPSessionIn) (*mcpsdk.CallToolResult, acpSessionDetail, error) {
if err := CheckToolFromCtx(ctx, "get_acp_session"); err != nil {
return nil, acpSessionDetail{}, err
}
c, err := deps.Caller.Resolve(ctx)
if err != nil {
return nil, acpSessionDetail{}, err
}
s, err := scopedACPSession(ctx, deps, c.UserID, c.IsAdmin, in.SessionID)
if err != nil {
return nil, acpSessionDetail{}, err
}
return nil, acpSessionDetailFrom(s), nil
})
}
type listACPSessionsIn struct {
All bool `json:"all,omitempty" jsonschema:"admin only: list sessions of all users"`
RequirementID string `json:"requirement_id,omitempty" jsonschema:"filter by requirement UUID"`
}
type listACPSessionsOut struct {
Sessions []acpSessionDetail `json:"sessions"`
}
func registerListACPSessions(srv *mcpsdk.Server, deps ServerDeps) {
mcpsdk.AddTool(srv,
&mcpsdk.Tool{Name: "list_acp_sessions", Description: "List ACP sessions of the caller (all=true for admins)."},
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in listACPSessionsIn) (*mcpsdk.CallToolResult, listACPSessionsOut, error) {
if err := CheckToolFromCtx(ctx, "list_acp_sessions"); err != nil {
return nil, listACPSessionsOut{}, err
}
c, err := deps.Caller.Resolve(ctx)
if err != nil {
return nil, listACPSessionsOut{}, err
}
f := ACPSessionFilter{All: in.All}
if in.RequirementID != "" {
rid, err := uuid.Parse(in.RequirementID)
if err != nil {
return nil, listACPSessionsOut{}, errs.Wrap(err, errs.CodeInvalidInput, "requirement_id parse")
}
f.RequirementID = &rid
}
ss, err := deps.ACP.ListSessions(ctx, c.UserID, c.IsAdmin, f)
if err != nil {
return nil, listACPSessionsOut{}, err
}
out := listACPSessionsOut{Sessions: make([]acpSessionDetail, 0, len(ss))}
for i := range ss {
// 与 list_projects 一致:scope 外的条目静默跳过。
if err := CheckProjectFromCtx(ctx, ss[i].ProjectID); err != nil {
continue
}
out.Sessions = append(out.Sessions, acpSessionDetailFrom(&ss[i]))
}
return nil, out, nil
})
}
func registerTerminateACPSession(srv *mcpsdk.Server, deps ServerDeps) {
mcpsdk.AddTool(srv,
&mcpsdk.Tool{Name: "terminate_acp_session", Description: "Terminate a running ACP session."},
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getACPSessionIn) (*mcpsdk.CallToolResult, okOut, error) {
if err := CheckToolFromCtx(ctx, "terminate_acp_session"); err != nil {
return nil, okOut{}, err
}
c, err := deps.Caller.Resolve(ctx)
if err != nil {
return nil, okOut{}, err
}
s, err := scopedACPSession(ctx, deps, c.UserID, c.IsAdmin, in.SessionID)
if err != nil {
return nil, okOut{}, err
}
if err := deps.ACP.TerminateSession(ctx, c.UserID, c.IsAdmin, s.ID); err != nil {
return nil, okOut{}, err
}
return nil, okOut{OK: true}, nil
})
}
// ===== list_pending_permissions =====
type permissionDetail struct {
ID string `json:"id"`
SessionID string `json:"session_id"`
AgentRequestID string `json:"agent_request_id"`
ToolName string `json:"tool_name"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
}
type listPendingPermissionsOut struct {
Permissions []permissionDetail `json:"permissions"`
}
func registerListPendingPermissions(srv *mcpsdk.Server, deps ServerDeps) {
mcpsdk.AddTool(srv,
&mcpsdk.Tool{Name: "list_pending_permissions", Description: "List pending permission requests of an ACP session (read-only; approval stays with humans in the web console)."},
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getACPSessionIn) (*mcpsdk.CallToolResult, listPendingPermissionsOut, error) {
if err := CheckToolFromCtx(ctx, "list_pending_permissions"); err != nil {
return nil, listPendingPermissionsOut{}, err
}
c, err := deps.Caller.Resolve(ctx)
if err != nil {
return nil, listPendingPermissionsOut{}, err
}
s, err := scopedACPSession(ctx, deps, c.UserID, c.IsAdmin, in.SessionID)
if err != nil {
return nil, listPendingPermissionsOut{}, err
}
ps, err := deps.ACP.ListPendingPermissions(ctx, c.UserID, c.IsAdmin, s.ID)
if err != nil {
return nil, listPendingPermissionsOut{}, err
}
out := listPendingPermissionsOut{Permissions: make([]permissionDetail, 0, len(ps))}
for _, p := range ps {
out.Permissions = append(out.Permissions, permissionDetail{
ID: p.ID.String(), SessionID: p.SessionID.String(),
AgentRequestID: p.AgentRequestID, ToolName: p.ToolName,
Status: p.Status,
CreatedAt: p.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
})
}
return nil, out, nil
})
}