package executor import ( "context" "crypto/rand" "encoding/hex" "encoding/json" "errors" "net/http" "strings" "sync" "github.com/modelcontextprotocol/go-sdk/mcp" ) // Registry maps per-task MCP bearer tokens to a built agent MCP server. A token // is minted when a runner spawns an agent subprocess and revoked when it exits, // so the server resolves task context from the token alone and never trusts an // agent-supplied task ID. type Registry struct { mu sync.RWMutex servers map[string]*mcp.Server } func NewRegistry() *Registry { return &Registry{servers: make(map[string]*mcp.Server)} } // Mint creates a token bound to a freshly built MCP server for ch. func (r *Registry) Mint(ch AgentChannel) (string, error) { buf := make([]byte, 32) if _, err := rand.Read(buf); err != nil { return "", err } token := hex.EncodeToString(buf) r.mu.Lock() r.servers[token] = newAgentServer(ch) r.mu.Unlock() return token, nil } func (r *Registry) server(token string) (*mcp.Server, bool) { r.mu.RLock() defer r.mu.RUnlock() s, ok := r.servers[token] return s, ok } func (r *Registry) Revoke(token string) { r.mu.Lock() delete(r.servers, token) r.mu.Unlock() } type askUserInput struct { Question string `json:"question" jsonschema:"the question to ask the user; phrase it as a real question ending with a question mark"` Options []string `json:"options,omitempty" jsonschema:"optional list of suggested answer choices"` } type reportSummaryInput struct { Summary string `json:"summary" jsonschema:"a 2-5 sentence summary of what you did and the outcome"` } type spawnSubtaskInput struct { Name string `json:"name" jsonschema:"short descriptive name for the subtask"` Instructions string `json:"instructions" jsonschema:"complete instructions for the subtask agent"` Model string `json:"model,omitempty" jsonschema:"optional model override, e.g. sonnet or opus"` MaxBudgetUSD float64 `json:"max_budget_usd,omitempty" jsonschema:"optional budget cap in USD"` } type recordProgressInput struct { Message string `json:"message" jsonschema:"a short progress note describing what you are doing"` } func textResult(text string) *mcp.CallToolResult { return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: text}}} } // newAgentServer builds an MCP server exposing the four agent tools bound to ch. func newAgentServer(ch AgentChannel) *mcp.Server { s := mcp.NewServer(&mcp.Implementation{Name: "claudomator", Version: "1"}, nil) mcp.AddTool(s, &mcp.Tool{ Name: "ask_user", Description: "Ask the user a question when you genuinely need a decision to proceed. Your turn ends after calling this; the task is resumed with the user's answer. Prefer making a reasonable decision and noting it in report_summary over asking.", }, func(ctx context.Context, _ *mcp.CallToolRequest, in askUserInput) (*mcp.CallToolResult, any, error) { q := map[string]any{"text": in.Question} if len(in.Options) > 0 { q["options"] = in.Options } payload, _ := json.Marshal(q) ans, err := ch.AskUser(ctx, string(payload)) if errors.Is(err, ErrAgentBlocked) { return textResult("Question recorded. End your turn now without calling any more tools; the task will be resumed once the user answers."), nil, nil } if err != nil { return nil, nil, err } return textResult(ans), nil, nil }) mcp.AddTool(s, &mcp.Tool{ Name: "report_summary", Description: "Record a concise summary of what you accomplished. Call this before finishing.", }, func(ctx context.Context, _ *mcp.CallToolRequest, in reportSummaryInput) (*mcp.CallToolResult, any, error) { if err := ch.ReportSummary(ctx, in.Summary); err != nil { return nil, nil, err } return textResult("Summary recorded."), nil, nil }) mcp.AddTool(s, &mcp.Tool{ Name: "spawn_subtask", Description: "Create a child task to be executed separately. Use this to break large work into focused pieces, then finish your turn.", }, func(ctx context.Context, _ *mcp.CallToolRequest, in spawnSubtaskInput) (*mcp.CallToolResult, any, error) { id, err := ch.SpawnSubtask(ctx, SubtaskSpec{ Name: in.Name, Instructions: in.Instructions, Model: in.Model, MaxBudgetUSD: in.MaxBudgetUSD, }) if err != nil { return nil, nil, err } return textResult("Created subtask " + id), nil, nil }) mcp.AddTool(s, &mcp.Tool{ Name: "record_progress", Description: "Record a short progress note that appears in the task timeline.", }, func(ctx context.Context, _ *mcp.CallToolRequest, in recordProgressInput) (*mcp.CallToolResult, any, error) { if err := ch.RecordProgress(ctx, in.Message); err != nil { return nil, nil, err } return textResult("Noted."), nil, nil }) return s } func bearerToken(r *http.Request) string { h := r.Header.Get("Authorization") if h == "" { return "" } return strings.TrimSpace(strings.TrimPrefix(h, "Bearer ")) } // NewAgentMCPHandler returns the HTTP handler for the per-task agent MCP server. // It resolves the request's bearer token to the server for that task's run; // unknown tokens yield a 400 from the underlying handler. func NewAgentMCPHandler(reg *Registry) http.Handler { return mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server { s, ok := reg.server(bearerToken(r)) if !ok { return nil } return s }, nil) }