You've already forked agentic-coding-workflow
695 lines
25 KiB
Go
695 lines
25 KiB
Go
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
|
|
LastStopReason *string
|
|
StartedAt time.Time
|
|
EndedAt *time.Time
|
|
// 成本核算(只读):运行时累计 token/cost + 有效预算上限。
|
|
PromptTokens int64
|
|
CompletionTokens int64
|
|
ThinkingTokens int64
|
|
TotalCostUSD float64
|
|
BudgetMaxCostUSD *float64
|
|
BudgetMaxTokens *int64
|
|
}
|
|
|
|
// ACPTurn 是 ACP 回合记录的 MCP 投影(只读)。
|
|
type ACPTurn struct {
|
|
TurnIndex int
|
|
Status string
|
|
StopReason *string
|
|
UpdateCount int
|
|
StartedAt time.Time
|
|
CompletedAt *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
|
|
}
|
|
|
|
// ACPRunDashboardInput 是 run-history 汇总入参(service 在 caller scope 内归一化)。
|
|
type ACPRunDashboardInput struct {
|
|
ProjectID *uuid.UUID
|
|
RequirementID *uuid.UUID
|
|
From time.Time
|
|
To time.Time
|
|
}
|
|
|
|
// ACPRunDashboard 是 run-history 汇总(计数 + 成功率 + 成本 + 时长 + 按天序列)。
|
|
type ACPRunDashboard struct {
|
|
From time.Time
|
|
To time.Time
|
|
Total int64
|
|
Succeeded int64
|
|
Crashed int64
|
|
Active int64
|
|
SuccessRate float64
|
|
TotalCostUSD float64
|
|
TotalTokens int64
|
|
AvgDurationSeconds float64
|
|
ByDay []ACPRunDayRow
|
|
}
|
|
|
|
// ACPRunDayRow 是 run-history 按天的一行。
|
|
type ACPRunDayRow struct {
|
|
Day time.Time
|
|
Total int64
|
|
Succeeded int64
|
|
Crashed int64
|
|
TotalCostUSD float64
|
|
}
|
|
|
|
// 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)
|
|
// ListPendingApprovals 返回调用者跨所有会话的待审权限请求(HITL inbox,read-only)。
|
|
ListPendingApprovals(ctx context.Context, userID uuid.UUID, isAdmin bool) ([]ACPPermission, error)
|
|
ListSessionTurns(ctx context.Context, userID uuid.UUID, isAdmin bool, sessionID uuid.UUID) ([]ACPTurn, error)
|
|
// GetRunDashboard 返回 run-history 汇总(owner scope;admin 全量),read-only。
|
|
GetRunDashboard(ctx context.Context, userID uuid.UUID, isAdmin bool, in ACPRunDashboardInput) (*ACPRunDashboard, 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)
|
|
registerListPendingApprovals(srv, deps)
|
|
registerGetACPSessionTurns(srv, deps)
|
|
registerGetRunDashboard(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"`
|
|
LastStopReason string `json:"last_stop_reason,omitempty"`
|
|
StartedAt string `json:"started_at"`
|
|
EndedAt string `json:"ended_at,omitempty"`
|
|
// 成本核算(只读)。
|
|
PromptTokens int64 `json:"prompt_tokens"`
|
|
CompletionTokens int64 `json:"completion_tokens"`
|
|
ThinkingTokens int64 `json:"thinking_tokens"`
|
|
TotalCostUSD float64 `json:"total_cost_usd"`
|
|
BudgetMaxCostUSD *float64 `json:"budget_max_cost_usd,omitempty"`
|
|
BudgetMaxTokens *int64 `json:"budget_max_tokens,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"),
|
|
PromptTokens: s.PromptTokens,
|
|
CompletionTokens: s.CompletionTokens,
|
|
ThinkingTokens: s.ThinkingTokens,
|
|
TotalCostUSD: s.TotalCostUSD,
|
|
BudgetMaxCostUSD: s.BudgetMaxCostUSD,
|
|
BudgetMaxTokens: s.BudgetMaxTokens,
|
|
}
|
|
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.LastStopReason != nil {
|
|
d.LastStopReason = *s.LastStopReason
|
|
}
|
|
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
|
|
})
|
|
}
|
|
|
|
// ===== list_pending_approvals (cross-session HITL inbox) =====
|
|
|
|
func registerListPendingApprovals(srv *mcpsdk.Server, deps ServerDeps) {
|
|
mcpsdk.AddTool(srv,
|
|
&mcpsdk.Tool{Name: "list_pending_approvals", Description: "List the caller's pending tool-permission requests across all their ACP sessions (read-only HITL inbox; approval stays with humans in the web console)."},
|
|
func(ctx context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, listPendingPermissionsOut, error) {
|
|
if err := CheckToolFromCtx(ctx, "list_pending_approvals"); err != nil {
|
|
return nil, listPendingPermissionsOut{}, err
|
|
}
|
|
c, err := deps.Caller.Resolve(ctx)
|
|
if err != nil {
|
|
return nil, listPendingPermissionsOut{}, err
|
|
}
|
|
ps, err := deps.ACP.ListPendingApprovals(ctx, c.UserID, c.IsAdmin)
|
|
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
|
|
})
|
|
}
|
|
|
|
// ===== get_acp_session_turns =====
|
|
|
|
type acpTurnDetail struct {
|
|
TurnIndex int `json:"turn_index"`
|
|
Status string `json:"status"`
|
|
StopReason string `json:"stop_reason,omitempty"`
|
|
UpdateCount int `json:"update_count"`
|
|
StartedAt string `json:"started_at"`
|
|
CompletedAt string `json:"completed_at,omitempty"`
|
|
}
|
|
type getACPSessionTurnsOut struct {
|
|
Turns []acpTurnDetail `json:"turns"`
|
|
}
|
|
|
|
func registerGetACPSessionTurns(srv *mcpsdk.Server, deps ServerDeps) {
|
|
mcpsdk.AddTool(srv,
|
|
&mcpsdk.Tool{Name: "get_acp_session_turns", Description: "List the agent turns of an ACP session (read-only; turn_index, status, stop_reason, update_count, timestamps)."},
|
|
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getACPSessionIn) (*mcpsdk.CallToolResult, getACPSessionTurnsOut, error) {
|
|
if err := CheckToolFromCtx(ctx, "get_acp_session_turns"); err != nil {
|
|
return nil, getACPSessionTurnsOut{}, err
|
|
}
|
|
c, err := deps.Caller.Resolve(ctx)
|
|
if err != nil {
|
|
return nil, getACPSessionTurnsOut{}, err
|
|
}
|
|
s, err := scopedACPSession(ctx, deps, c.UserID, c.IsAdmin, in.SessionID)
|
|
if err != nil {
|
|
return nil, getACPSessionTurnsOut{}, err
|
|
}
|
|
ts, err := deps.ACP.ListSessionTurns(ctx, c.UserID, c.IsAdmin, s.ID)
|
|
if err != nil {
|
|
return nil, getACPSessionTurnsOut{}, err
|
|
}
|
|
out := getACPSessionTurnsOut{Turns: make([]acpTurnDetail, 0, len(ts))}
|
|
for _, t := range ts {
|
|
d := acpTurnDetail{
|
|
TurnIndex: t.TurnIndex,
|
|
Status: t.Status,
|
|
UpdateCount: t.UpdateCount,
|
|
StartedAt: t.StartedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
}
|
|
if t.StopReason != nil {
|
|
d.StopReason = *t.StopReason
|
|
}
|
|
if t.CompletedAt != nil {
|
|
d.CompletedAt = t.CompletedAt.Format("2006-01-02T15:04:05Z07:00")
|
|
}
|
|
out.Turns = append(out.Turns, d)
|
|
}
|
|
return nil, out, nil
|
|
})
|
|
}
|
|
|
|
// ===== get_run_dashboard (run-history rollup) =====
|
|
|
|
type getRunDashboardIn struct {
|
|
// 可选过滤;留空表示不按该维度过滤。from/to 为 RFC3339;留空走默认 30 天窗口。
|
|
ProjectID string `json:"project_id,omitempty"`
|
|
RequirementID string `json:"requirement_id,omitempty"`
|
|
From string `json:"from,omitempty"`
|
|
To string `json:"to,omitempty"`
|
|
}
|
|
|
|
type runDayDetail struct {
|
|
Day string `json:"day"`
|
|
Total int64 `json:"total"`
|
|
Succeeded int64 `json:"succeeded"`
|
|
Crashed int64 `json:"crashed"`
|
|
TotalCostUSD float64 `json:"total_cost_usd"`
|
|
}
|
|
|
|
type getRunDashboardOut struct {
|
|
From string `json:"from"`
|
|
To string `json:"to"`
|
|
Total int64 `json:"total"`
|
|
Succeeded int64 `json:"succeeded"`
|
|
Crashed int64 `json:"crashed"`
|
|
Active int64 `json:"active"`
|
|
SuccessRate float64 `json:"success_rate"`
|
|
TotalCostUSD float64 `json:"total_cost_usd"`
|
|
TotalTokens int64 `json:"total_tokens"`
|
|
AvgDurationSeconds float64 `json:"avg_duration_seconds"`
|
|
ByDay []runDayDetail `json:"by_day"`
|
|
}
|
|
|
|
func registerGetRunDashboard(srv *mcpsdk.Server, deps ServerDeps) {
|
|
mcpsdk.AddTool(srv,
|
|
&mcpsdk.Tool{Name: "get_run_dashboard", Description: "Get a run-history rollup of ACP sessions (read-only): counts, success rate, total cost/tokens, avg duration, and a per-day series. Scoped to the caller's own sessions (admins see all). Optional filters: project_id, requirement_id, from/to (RFC3339)."},
|
|
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getRunDashboardIn) (*mcpsdk.CallToolResult, getRunDashboardOut, error) {
|
|
if err := CheckToolFromCtx(ctx, "get_run_dashboard"); err != nil {
|
|
return nil, getRunDashboardOut{}, err
|
|
}
|
|
c, err := deps.Caller.Resolve(ctx)
|
|
if err != nil {
|
|
return nil, getRunDashboardOut{}, err
|
|
}
|
|
var di ACPRunDashboardInput
|
|
if in.ProjectID != "" {
|
|
id, perr := uuid.Parse(in.ProjectID)
|
|
if perr != nil {
|
|
return nil, getRunDashboardOut{}, errs.Wrap(perr, errs.CodeInvalidInput, "project_id parse")
|
|
}
|
|
di.ProjectID = &id
|
|
}
|
|
if in.RequirementID != "" {
|
|
id, perr := uuid.Parse(in.RequirementID)
|
|
if perr != nil {
|
|
return nil, getRunDashboardOut{}, errs.Wrap(perr, errs.CodeInvalidInput, "requirement_id parse")
|
|
}
|
|
di.RequirementID = &id
|
|
}
|
|
if in.From != "" {
|
|
t, terr := time.Parse(time.RFC3339, in.From)
|
|
if terr != nil {
|
|
return nil, getRunDashboardOut{}, errs.Wrap(terr, errs.CodeInvalidInput, "from parse")
|
|
}
|
|
di.From = t
|
|
}
|
|
if in.To != "" {
|
|
t, terr := time.Parse(time.RFC3339, in.To)
|
|
if terr != nil {
|
|
return nil, getRunDashboardOut{}, errs.Wrap(terr, errs.CodeInvalidInput, "to parse")
|
|
}
|
|
di.To = t
|
|
}
|
|
d, err := deps.ACP.GetRunDashboard(ctx, c.UserID, c.IsAdmin, di)
|
|
if err != nil {
|
|
return nil, getRunDashboardOut{}, err
|
|
}
|
|
out := getRunDashboardOut{
|
|
From: d.From.Format("2006-01-02T15:04:05Z07:00"),
|
|
To: d.To.Format("2006-01-02T15:04:05Z07:00"),
|
|
Total: d.Total,
|
|
Succeeded: d.Succeeded,
|
|
Crashed: d.Crashed,
|
|
Active: d.Active,
|
|
SuccessRate: d.SuccessRate,
|
|
TotalCostUSD: d.TotalCostUSD,
|
|
TotalTokens: d.TotalTokens,
|
|
AvgDurationSeconds: d.AvgDurationSeconds,
|
|
ByDay: make([]runDayDetail, 0, len(d.ByDay)),
|
|
}
|
|
for _, r := range d.ByDay {
|
|
out.ByDay = append(out.ByDay, runDayDetail{
|
|
Day: r.Day.Format("2006-01-02T15:04:05Z07:00"),
|
|
Total: r.Total,
|
|
Succeeded: r.Succeeded,
|
|
Crashed: r.Crashed,
|
|
TotalCostUSD: r.TotalCostUSD,
|
|
})
|
|
}
|
|
return nil, out, nil
|
|
})
|
|
}
|