1package mcp23import (4 "context"5 "encoding/json"67 sdk "github.com/modelcontextprotocol/go-sdk/mcp"89 "src.rest/cat/012/internal/diff"10 "src.rest/cat/012/internal/sheet"11)1213// Live mode (docs/agents/live.md): the server runs inside a 012 session14// with its workbook on someone's screen, and the agent is one of the15// participants of the session's room. Every tool works on that one16// workbook, whose Backend is a Live: reads see it as it is on the17// screen, and a change is the agent's, a suggestion the person accepts18// or rejects unless they let agents edit directly. Live mode adds the19// tools only a live workbook has: focus, undo, ask and suggestions.2021// Live is a workbook open in a session, as the agent attached to it22// works on it.23type Live interface {24 Backend25 // Make makes a change as the agent's, or suggests it.26 Make(ctx context.Context, m Making) (Made, error)27 // Focus moves the agent's pointer to ref, for the person to see28 // where it's looking, and says where that is.29 Focus(ctx context.Context, ref string) (string, error)30 // Undo takes back the agent's latest step, saying what it was.31 Undo(ctx context.Context) (string, error)32 // Ask asks the person a question, as an elicitation request asks a33 // host's user, and returns their answer.34 Ask(ctx context.Context, p *sdk.ElicitParams) (*sdk.ElicitResult, error)35 // Suggestions are the agent's suggestions and what became of them.36 Suggestions(ctx context.Context) ([]Suggestion, error)37 // RunCell has the person's session run a notebook cell, once they38 // allow it, and says what happens next.39 RunCell(ctx context.Context, notebook string, cell int) (string, error)40 // Scope is what the person let the agent change, in words.41 Scope() string42}4344// Making is a change a tool asks for: its label, the agent's message45// shown with it, whether only to see it, and the change itself.46type Making struct {47 Label, Message string48 DryRun bool49 Fn func(*sheet.Workbook) error50}5152// Made is what came of a change: what it changes, whether it was made53// on the workbook or waits as a suggestion, and news of the agent's54// earlier suggestions.55type Made struct {56 Changes []diff.Change57 Applied bool58 Suggestion int59 Notices []string60}6162// Suggestion is one of the agent's suggestions as the suggestions tool63// lists it.64type Suggestion struct {65 ID int `json:"id"`66 Label string `json:"label"`67 Message string `json:"message,omitempty"`68 State string `json:"state" jsonschema:"pending, accepted, accepted in part or rejected"`69 Cells int `json:"cells" jsonschema:"the cells it sets"`70 Accepted int `json:"accepted" jsonschema:"the cells the person accepted"`71 Rejected int `json:"rejected" jsonschema:"the cells the person rejected"`72}7374const liveInstructions = `This server is attached to a workbook open on a person's screen in 012 (live mode): every tool works on that one workbook, so leave path out. Reads see it as it is now, the person's edits included.75Your changes are suggestions unless the person lets agents edit directly: the cells you'd change are marked on their screen, and they accept or reject them, whole or cell by cell. A write returns the suggestion's number; the suggestions tool says what became of each, and news of them also comes back with your next write. Pass message with a write to tell the person why.76Call focus to move your pointer where you're working, so the person sees where you're looking. undo takes back your own latest change, never the person's. ask asks the person a question and waits for the answer, as elicitation does: use it before anything they might not expect.77You may change only what the person let you (the scope); a change outside it is refused with the reason.`7879type focusIn struct {80 Ref string `json:"ref" jsonschema:"the cell or range to point at: B7, Q3!A1:C9, a named range or a table"`81}8283type focusOut struct {84 At string `json:"at" jsonschema:"where the pointer is, with its sheet"`85}8687type undoOut struct {88 Undone string `json:"undone" jsonschema:"what the step undone did"`89}9091type askIn struct {92 Message string `json:"message" jsonschema:"the question, shown to the person"`93 RequestedSchema map[string]any `json:"requestedSchema,omitempty" jsonschema:"what to ask for, as an elicitation request's: an object schema of top-level string, number, integer and boolean properties, strings with enum for a choice; none for a yes or no question"`94}9596type askOut struct {97 Action string `json:"action" jsonschema:"accept, decline or cancel, as an elicitation's answer"`98 Content map[string]any `json:"content,omitempty" jsonschema:"with accept, the answers by property"`99}100101type suggestionsOut struct {102 Scope string `json:"scope" jsonschema:"what the person let you change"`103 Suggestions []Suggestion `json:"suggestions"`104}105106type liveRunOut struct {107 Status string `json:"status"`108}109110// SuggestionsURI is the resource listing the agent's suggestions, as111// the suggestions tool does; clients subscribed to it are told when the112// person settles one (SuggestionsChanged).113const SuggestionsURI = scheme + "live/suggestions"114115// SuggestionsChanged tells clients subscribed to SuggestionsURI that116// the person settled a suggestion.117func (s *Server) SuggestionsChanged(ctx context.Context) {118 s.ResourceUpdated(ctx, &sdk.ResourceUpdatedNotificationParams{URI: SuggestionsURI})119}120121// addLiveTools adds the tools of a live workbook, and its suggestions122// resource.123func (s *Server) addLiveTools(l Live) {124 s.AddResource(&sdk.Resource{URI: SuggestionsURI, Name: "suggestions", MIMEType: "application/json",125 Description: "Your suggestions and what became of each, as the suggestions tool lists them; subscribe to hear when the person settles one"},126 func(ctx context.Context, _ *sdk.ReadResourceRequest) (*sdk.ReadResourceResult, error) {127 list, err := l.Suggestions(ctx)128 if err != nil {129 return nil, err130 }131 data, _ := json.Marshal(suggestionsOut{Scope: l.Scope(), Suggestions: list})132 return &sdk.ReadResourceResult{Contents: []*sdk.ResourceContents{{URI: SuggestionsURI, MIMEType: "application/json", Text: string(data)}}}, nil133 })134 tool(s, &sdk.Tool{Name: "focus", Annotations: &sdk.ToolAnnotations{IdempotentHint: true, OpenWorldHint: ptr(false)},135 Description: "Move your pointer to a cell or range, where the person sees it with your name, to show where you're working."},136 func(ctx context.Context, _ *sdk.CallToolRequest, in focusIn) (*sdk.CallToolResult, focusOut, error) {137 at, err := l.Focus(ctx, in.Ref)138 return nil, focusOut{At: at}, err139 })140 tool(s, &sdk.Tool{Name: "undo", Annotations: &sdk.ToolAnnotations{DestructiveHint: ptr(true), OpenWorldHint: ptr(false)},141 Description: "Take back your own latest change that was made (accepted, or made directly), as Edit > Undo does for the person: never theirs."},142 func(ctx context.Context, _ *sdk.CallToolRequest, _ struct{}) (*sdk.CallToolResult, undoOut, error) {143 label, err := l.Undo(ctx)144 return nil, undoOut{Undone: label}, err145 })146 tool(s, &sdk.Tool{Name: "ask", Annotations: &sdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptr(false)},147 Description: "Ask the person a question on their screen and wait for the answer: yes or no, a choice, or values, as an elicitation request asks. Use it before a change they might not expect (\"Overwrite B2:B40?\")."},148 func(ctx context.Context, _ *sdk.CallToolRequest, in askIn) (*sdk.CallToolResult, askOut, error) {149 res, err := l.Ask(ctx, &sdk.ElicitParams{Message: in.Message, RequestedSchema: in.RequestedSchema})150 if err != nil {151 return nil, askOut{}, err152 }153 return nil, askOut{Action: res.Action, Content: res.Content}, nil154 })155 tool(s, &sdk.Tool{Name: "suggestions", Annotations: readOnly,156 Description: "Your suggestions and what became of each: pending, accepted, accepted in part or rejected, with how many cells; and what the person let you change."},157 func(ctx context.Context, _ *sdk.CallToolRequest, _ struct{}) (*sdk.CallToolResult, suggestionsOut, error) {158 list, err := l.Suggestions(ctx)159 if list == nil {160 list = []Suggestion{}161 }162 return nil, suggestionsOut{Scope: l.Scope(), Suggestions: list}, err163 })164 tool(s, &sdk.Tool{Name: "run_notebook_cell", Annotations: &sdk.ToolAnnotations{DestructiveHint: ptr(false), OpenWorldHint: ptr(true)},165 Description: "Have the person's session run a notebook tab's code cell with nushell, once they allow it: its output arrives in the workbook as when they run it, to read with describe or read_range."},166 func(ctx context.Context, _ *sdk.CallToolRequest, in runCellIn) (*sdk.CallToolResult, liveRunOut, error) {167 status, err := l.RunCell(ctx, in.Notebook, in.Cell)168 return nil, liveRunOut{Status: status}, err169 })170}