cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

cat/module-path
012 / internal / mcp / views.go
views.go 5.5K
1package mcp23import (4 "context"56 sdk "github.com/modelcontextprotocol/go-sdk/mcp"78 "src.rest/cat/012/internal/fileio"9 "src.rest/cat/012/internal/headless"10 "src.rest/cat/012/internal/sheet"11)1213// Views are the range read_range read and the chart create_chart made,14// drawn as 012 draws them in a sandboxed frame beside the result, by15// hosts that implement either of the two ways of doing that:16//17// - MCP Apps (the io.modelcontextprotocol/ui extension, spec18// 2026-01-26): the tool's _meta.ui.resourceUri names ui://012/view,19// text/html;profile=mcp-app, and the page speaks the extension's20// messages over postMessage (ui/initialize, the tool's result, the21// host's theme and room, the page's size).22// - The OpenAI Apps SDK (ChatGPT, Codex): the tool's23// _meta["openai/outputTemplate"] names ui://012/view.skybridge, the24// same page as text/html+skybridge, which reads window.openai25// (toolResponseMetadata, theme, maxHeight, displayMode) instead.26//27// The page is the HTML export's with nothing in it yet (fonts,28// palettes, the pointer's script). What it draws is in the result's29// _meta under viewKey, for every client, since both kinds of hosts hand30// a result's _meta to the view and neither shows it to the model: its31// title, where it is and the grid's or chart's HTML. structuredContent32// stays the tool's result, what the model reads.33const (34 uiExtension = "io.modelcontextprotocol/ui"35 uiMIME = "text/html;profile=mcp-app"36 skybridgeMIME = "text/html+skybridge"37 viewURI = "ui://012/view"38 viewKey = "o12/view"39 skybridgeURI = "ui://012/view.skybridge"40 // maxViewRows bounds the rows a range's view draws.41 maxViewRows = 20042)4344// View is what a view draws of a result, in its _meta under viewKey:45// the workbook's name, what the name box shows before a cell is46// pointed at (a chart's data), the range or the chart's kind, sheet and47// data, and the grid or chart as the HTML export draws it.48type View struct {49 Title string `json:"title"`50 Name string `json:"name,omitempty"`51 Where string `json:"where"`52 HTML string `json:"html"`53}5455// viewResourceMeta says how hosts should frame the view, in both56// dialects: a border, and no network (an empty content security57// policy's lists).58var viewResourceMeta = sdk.Meta{59 "ui": map[string]any{"prefersBorder": true, "csp": map[string]any{}},60 "openai/widgetPrefersBorder": true,61 "openai/widgetCSP": map[string]any{"connect_domains": []string{}, "resource_domains": []string{}},62 "openai/widgetDescription": "The range or chart the tool returned, drawn as 012's grid: the model need not repeat it.",63}6465func (s *Server) addViews() {66 for _, r := range []struct{ uri, mime string }{{viewURI, uiMIME}, {skybridgeURI, skybridgeMIME}} {67 s.AddResource(&sdk.Resource{URI: r.uri, Name: "012 view", Title: "012", MIMEType: r.mime, Meta: viewResourceMeta,68 Description: "The range or chart a tool returned, drawn as 012 draws it"},69 func(context.Context, *sdk.ReadResourceRequest) (*sdk.ReadResourceResult, error) {70 return &sdk.ReadResourceResult{Contents: []*sdk.ResourceContents{{URI: r.uri, MIMEType: r.mime, Text: viewPage(), Meta: viewResourceMeta}}}, nil71 })72 }73}7475// viewPage is the view's page: the panel, an empty place for the view76// and the scripts.77func viewPage() string {78 return fileio.HTMLDocument("012", "<style>"+viewCSS+"</style>"+fileio.HTMLPanel("012", "", "")+`<div id="o12-view"></div>`, viewScript)79}8081// viewMeta is what a tool drawn in the view says of it: the resource82// under MCP Apps' key and its first drafts' flat one, and the Apps83// SDK's template and the status lines it shows while the tool runs.84// The view calls no tools, so openai/widgetAccessible stays false.85func viewMeta(invoking, invoked string) sdk.Meta {86 return sdk.Meta{"ui": map[string]any{"resourceUri": viewURI}, "ui/resourceUri": viewURI,87 "openai/outputTemplate": skybridgeURI, "openai/toolInvocation/invoking": invoking, "openai/toolInvocation/invoked": invoked}88}8990// viewResult is a result carrying v in its _meta, nil without one.91func viewResult(v *View) *sdk.CallToolResult {92 if v == nil {93 return nil94 }95 return &sdk.CallToolResult{Meta: sdk.Meta{viewKey: v}}96}9798// rangeView draws the rows read_range returned of t, at most99// maxViewRows.100func rangeView(b *book, t headless.Target, rows int) *View {101 r := t.Range102 if t.Whole {103 var ok bool104 if r, ok = t.Sheet.UsedRange(); !ok {105 r = sheet.Rect{}106 }107 }108 r.To.Row = min(r.To.Row, r.From.Row+max(rows, 1)-1, r.From.Row+maxViewRows-1)109 snap := fileio.Snap(t.Sheet, r, t.Sheet.Name())110 grid, _ := fileio.HTMLGrid(snap)111 return &View{Title: b.Name(), Where: sheet.Qualified(t.Sheet.Name(), snap.Range), HTML: grid}112}113114// chartView draws the chart create_chart made in b.115func chartView(b *book, w *sheet.Workbook, made headless.ChartMade) *View {116 c, ok := fileio.ChartSnap(w.Lookup(made.Sheet), made.Number-1)117 if !ok {118 return nil119 }120 data := sheet.Qualified(made.Sheet, c.Data)121 return &View{Title: b.Name(), Name: c.Data.String(), Where: c.Type.Title() + " chart of " + data, HTML: fileio.HTMLChart(c)}122}123124// viewCSS fits the export's page to a frame: the panel on top, the125// grid scrolling in what room the host gives, its headers sticking to126// the grid's edges.127const viewCSS = `html,body{overflow:hidden}128.o12{width:auto;min-width:0}129#o12-view{overflow-x:auto}130#o12-view .sheet{overflow:auto;max-height:480px;overscroll-behavior:contain}131#o12-view .grid thead th{top:0}132#o12-view:empty::before{content:"Waiting for the tool's result";display:block;color:var(--muted);padding:0 9px;height:21px}133`