Skip to content

Commit a29e92e

Browse files
feat: implement interactive task editor and MCP server integration
1 parent 8e2590b commit a29e92e

14 files changed

Lines changed: 378 additions & 48 deletions

File tree

.goreleaser.yaml

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,24 +79,35 @@ release:
7979
draft: false
8080
prerelease: auto
8181

82-
homebrew_casks:
82+
brews:
8383
- name: kairo
8484

8585
ids:
8686
- kairo
8787

88-
binaries:
89-
- kairo
90-
9188
repository:
9289
owner: programmersd21
93-
name: kairo_tap
90+
name: homebrew-kairo
91+
branch: main
9492
token: "{{ .Env.TAP_GITHUB_TOKEN }}"
9593

96-
directory: Casks
97-
98-
commit_msg_template: "Brew cask update for {{ .ProjectName }} {{ .Tag }}"
94+
directory: Formula
9995

10096
homepage: "https://github.com/programmersd21/kairo"
10197
description: "Minimal, powerful task management"
98+
license: "MIT"
99+
100+
commit_author:
101+
name: goreleaserbot
102+
email: bot@goreleaser.com
103+
104+
commit_msg_template: "brew: update {{ .ProjectName }} to {{ .Tag }}"
105+
106+
install: |
107+
bin.install "kairo"
108+
109+
test: |
110+
system "#{bin}/kairo", "--version"
111+
112+
skip_upload: auto
102113

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
* **Markdown Preview Panel**: The task editor now features a side-by-side markdown preview panel (toggled with `ctrl+p` or automatically on wide screens) for real-time visualization of task descriptions.
13+
* **Plugin Notification API**: Connected the Lua `kairo.notify` function to the TUI status bar, allowing plugins to provide visual feedback directly to the user.
14+
* **Fixed Plugin Notifications**: Resolved an issue where plugin notifications were not appearing in the status bar due to missing async message handling.
15+
* **MCP Server Port Control**: Added support for running the built-in MCP server in SSE/HTTP mode on a specific port (`kairo mcp <port>`).
16+
* **MCP Configuration**: Added `mcp_port` setting to `config.toml` and `KAIRO_MCP_PORT` environment variable support for flexible port overrides and auto-start configuration.
17+
* **REAL MCP Server Enhancements**: Transformed the built-in MCP server into a professional-grade implementation with support for Resources (`tasks://all`), Prompts (`manage_tasks`), and expanded Tools (including `kairo_get_task` and `kairo_list_tags`).
1218
* **AI Total App Control**: Updated the AI Assistant's system prompt and tool definitions to enable seamless control over UI themes and Lua plugins.
1319
* **Help Menu Clarity**: Added dedicated keybinding information for the Import/Export menu to the global help screen.
1420

README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Pick your platform:
2222
### 🍺 macOS (Homebrew)
2323

2424
```bash
25-
brew tap programmersd21/kairo_tap && brew install --cask kairo
25+
brew install programmersd21/kairo/kairo
2626
```
2727

2828
### 🐧 Linux / macOS (curl)
@@ -96,7 +96,10 @@ Kairo is the missing middle:
9696

9797
* Lua plugin system (event hooks: task_create, task_update, app_start, etc.)
9898
* Headless CLI API for automation
99-
* MCP server exposing full task schema to AI agents
99+
* **Professional MCP Server**: Full Model Context Protocol implementation
100+
* **Tools**: CRUD operations, tag listing, and theme management
101+
* **Resources**: Direct access to JSON task data (`tasks://all`)
102+
* **Prompts**: Pre-configured AI workflows (`manage_tasks`)
100103
* Custom themes via Lua or config
101104

102105
### 🤖 AI (optional)
@@ -109,6 +112,7 @@ Kairo is the missing middle:
109112
### 🎨 Terminal UI
110113

111114
* Bento-style layout with Lip Gloss styling
115+
* **Markdown Preview**: Side-by-side real-time rendering in the task editor (`ctrl+p`)
112116
* 32 built-in themes (dark/light/hybrid)
113117
* Live theme switching (`t`)
114118
* Full-viewport rendering (no terminal bleed-through)
@@ -140,7 +144,8 @@ kairo api list --tag work
140144
kairo api update --id <id> --status done
141145
kairo export --format markdown
142146
kairo sync
143-
kairo mcp
147+
kairo mcp # stdio mode (local)
148+
kairo mcp 8080 # sse mode (remote)
144149
```
145150

146151
---

VERSION.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.3.1
1+
1.3.2

cmd/kairo/main.go

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"errors"
77
"fmt"
8+
"net/http"
89
"os"
910
"os/signal"
1011
"path/filepath"
@@ -143,7 +144,7 @@ func main() {
143144
}
144145
return
145146
case "mcp":
146-
if err := runMCP(ctx, svc); err != nil {
147+
if err := runMCP(ctx, svc, os.Args[2:]); err != nil {
147148
fmt.Fprintln(os.Stderr, "kairo mcp:", err)
148149
os.Exit(2)
149150
}
@@ -360,6 +361,7 @@ func runHelp(args []string) {
360361
fmt.Println(" completion Generate shell completion scripts")
361362
fmt.Println(" export Export tasks to JSON or Markdown")
362363
fmt.Println(" import Import tasks from JSON or Markdown")
364+
fmt.Println(" mcp Run the built-in MCP server")
363365
fmt.Println(" sync Sync tasks with Git repository")
364366
fmt.Println(" update Update Kairo to the latest version")
365367
fmt.Println(" version Show the current version")
@@ -394,12 +396,54 @@ func runHelp(args []string) {
394396
fmt.Println("Sync tasks with Git repository.")
395397
fmt.Println("\nUsage:")
396398
fmt.Println(" kairo sync")
399+
case "mcp":
400+
fmt.Println("Run the built-in Model Context Protocol (MCP) server.")
401+
fmt.Println("\nUsage:")
402+
fmt.Println(" kairo mcp [port]")
403+
fmt.Println("\nExample:")
404+
fmt.Println(" kairo mcp (Runs in Stdio mode for local AI agents)")
405+
fmt.Println(" kairo mcp 8080 (Runs in SSE/HTTP mode on port 8080)")
406+
fmt.Println("\nDescription:")
407+
fmt.Println(" Exposes your Kairo tasks and tools to AI agents using the MCP standard.")
408+
fmt.Println(" Stdio mode is used for local integration (e.g. Claude Desktop).")
409+
fmt.Println(" SSE mode allows remote connections via HTTP.")
397410
default:
398411
fmt.Printf("Unknown help topic %q\n", args[0])
399412
}
400413
}
401-
func runMCP(ctx context.Context, svc service.TaskService) error {
414+
func runMCP(ctx context.Context, svc service.TaskService, args []string) error {
402415
s := mcp.NewServer(svc)
416+
417+
cfg, _ := config.Load()
418+
port := os.Getenv("KAIRO_MCP_PORT")
419+
if port == "" {
420+
port = cfg.App.MCPPort
421+
}
422+
if len(args) > 0 {
423+
port = args[0]
424+
}
425+
426+
if port != "" {
427+
addr := ":" + port
428+
baseURL := "http://localhost" + addr
429+
sseServer := mcpserver.NewSSEServer(s, mcpserver.WithBaseURL(baseURL))
430+
431+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
432+
w.Header().Set("Access-Control-Allow-Origin", "*")
433+
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
434+
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
435+
if r.Method == "OPTIONS" {
436+
w.WriteHeader(http.StatusOK)
437+
return
438+
}
439+
sseServer.ServeHTTP(w, r)
440+
})
441+
442+
fmt.Printf("Starting Kairo MCP SSE server on %s (CORS enabled)\n", addr)
443+
fmt.Printf("SSE endpoint: %s/sse\n", baseURL)
444+
return http.ListenAndServe(addr, handler)
445+
}
446+
403447
server := mcpserver.NewStdioServer(s)
404448
return server.Listen(ctx, os.Stdin, os.Stdout)
405449
}

internal/app/model.go

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,15 @@ type Model struct {
145145

146146
statusText string
147147
isErr bool
148+
statusID int
148149

149150
updateAvailable *updateAvailableMsg
150151

151152
syncEngine *ksync.Engine
152153

153154
plugHost *plugins.Host
154155
plugCh chan struct{}
156+
statusCh chan statusMsg
155157
configCh chan config.Config
156158

157159
RainbowAnimationOffset int
@@ -283,9 +285,12 @@ func New(ctx context.Context, cfg config.Config, svc service.TaskService) (tea.M
283285
if dir != "" {
284286
_ = os.MkdirAll(dir, 0o755)
285287
m.plugHost = plugins.New(svc, dir)
288+
m.statusCh = make(chan statusMsg, 8)
286289
m.plugHost.SetNotifyFunc(func(msg string, isErr bool) {
287-
m.statusText = msg
288-
m.isErr = isErr
290+
select {
291+
case m.statusCh <- statusMsg{Message: msg, IsErr: isErr}:
292+
default:
293+
}
289294
})
290295
_ = m.plugHost.LoadAll()
291296

@@ -343,6 +348,9 @@ func (m *Model) Init() tea.Cmd {
343348
if m.plugCh != nil {
344349
cmds = append(cmds, m.listenPluginsCmd())
345350
}
351+
if m.statusCh != nil {
352+
cmds = append(cmds, m.listenStatusCmd())
353+
}
346354
if m.configCh != nil {
347355
cmds = append(cmds, m.listenConfigCmd())
348356
}
@@ -395,6 +403,20 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
395403
case errMsg:
396404
m.statusText = x.Err.Error()
397405
m.isErr = true
406+
m.statusID++
407+
return m, m.clearStatusCmd(m.statusID)
408+
409+
case statusMsg:
410+
m.statusText = x.Message
411+
m.isErr = x.IsErr
412+
m.statusID++
413+
return m, tea.Batch(m.listenStatusCmd(), m.clearStatusCmd(m.statusID))
414+
415+
case clearStatusMsg:
416+
if x.ID == m.statusID {
417+
m.statusText = ""
418+
m.isErr = false
419+
}
398420
return m, nil
399421

400422
case tasksLoadedMsg:
@@ -1377,7 +1399,11 @@ func (m *Model) startMCPCmd() tea.Cmd {
13771399
return nil
13781400
}
13791401
exe, _ := os.Executable()
1380-
cmd := exec.Command(exe, "mcp")
1402+
args := []string{"mcp"}
1403+
if m.cfg.App.MCPPort != "" {
1404+
args = append(args, m.cfg.App.MCPPort)
1405+
}
1406+
cmd := exec.Command(exe, args...)
13811407
if err := cmd.Start(); err != nil {
13821408
return mcpStatusMsg{Running: false}
13831409
}
@@ -2198,6 +2224,18 @@ func keymapMatch(b interface{ Keys() []string }, k tea.KeyMsg) bool {
21982224
}
21992225
return false
22002226
}
2227+
2228+
func (m *Model) listenStatusCmd() tea.Cmd {
2229+
return func() tea.Msg {
2230+
return <-m.statusCh
2231+
}
2232+
}
2233+
2234+
func (m *Model) clearStatusCmd(id int) tea.Cmd {
2235+
return tea.Tick(5*time.Second, func(t time.Time) tea.Msg {
2236+
return clearStatusMsg{ID: id}
2237+
})
2238+
}
22012239
func (m *Model) handleImportExportAction(action import_export_menu.Action, path string) tea.Cmd {
22022240
return func() tea.Msg {
22032241
taskAPI := api.New(m.svc)

internal/app/msg.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ type tasksLoadedMsg struct{ Tasks []core.Task }
88
type tagsLoadedMsg struct{ Tags []string }
99
type allTasksLoadedMsg struct{ Tasks []core.Task }
1010

11+
type statusMsg struct {
12+
Message string
13+
IsErr bool
14+
}
15+
16+
type clearStatusMsg struct {
17+
ID int
18+
}
19+
1120
type taskCreatedMsg struct{ Task core.Task }
1221
type taskUpdatedMsg struct{ Task core.Task }
1322
type taskDeletedMsg struct{ ID string }

internal/config/config.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type AppConfig struct {
2929
GeminiAPIKey string `toml:"gemini_api_key"`
3030
AIModel string `toml:"ai_model"`
3131
MCPEnabled bool `toml:"mcp_enabled"`
32+
MCPPort string `toml:"mcp_port"`
3233
}
3334

3435
type StorageConfig struct {
@@ -93,11 +94,13 @@ type KeymapConfig struct {
9394
func Default() Config {
9495
return Config{
9596
App: AppConfig{
96-
Theme: "catppuccin",
97-
VimMode: false,
98-
ShowHelp: true,
99-
Rainbow: false,
100-
AIModel: "gemini-3.1-flash-lite-preview",
97+
Theme: "catppuccin",
98+
VimMode: false,
99+
ShowHelp: true,
100+
Rainbow: false,
101+
AIModel: "gemini-3.1-flash-lite-preview",
102+
MCPEnabled: false,
103+
MCPPort: "8080",
101104
},
102105
Theme: ThemeConfig{
103106
Bg: "", // Use theme default

internal/lua/engine.go

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ import (
1616

1717
// Engine manages the Lua runtime and plugin execution
1818
type Engine struct {
19-
service service.TaskService
20-
hooks *hooks.Manager
21-
timeout time.Duration
19+
service service.TaskService
20+
hooks *hooks.Manager
21+
notifyFunc func(string, bool)
22+
timeout time.Duration
2223
}
2324

2425
// NewEngine creates a new Lua engine with the given service and hooks
@@ -30,6 +31,11 @@ func NewEngine(svc service.TaskService, hks *hooks.Manager) *Engine {
3031
}
3132
}
3233

34+
// SetNotifyFunc sets the notification callback for plugins
35+
func (e *Engine) SetNotifyFunc(f func(string, bool)) {
36+
e.notifyFunc = f
37+
}
38+
3339
// SetTimeout sets the execution timeout for plugin code
3440
func (e *Engine) SetTimeout(d time.Duration) {
3541
e.timeout = d
@@ -55,7 +61,7 @@ func (e *Engine) SetupKairoAPI(L *lua.LState) {
5561
L.SetField(kairo, "notify", L.NewFunction(e.luaNotify))
5662

5763
// Meta
58-
L.SetField(kairo, "version", lua.LString("1.3.1"))
64+
L.SetField(kairo, "version", lua.LString("1.3.2"))
5965

6066
// Set as global
6167
L.SetGlobal("kairo", kairo)
@@ -308,18 +314,17 @@ func (e *Engine) luaOff(L *lua.LState) int {
308314
return 0
309315
}
310316

311-
// luaNotify sends a notification (stub for now)
312-
// In production, this would emit to the UI
317+
// luaNotify sends a notification
313318
func (e *Engine) luaNotify(L *lua.LState) int {
314319
msg := L.CheckString(1)
315320
isErr := false
316321
if L.GetTop() > 1 {
317322
isErr = lua.LVAsBool(L.Get(2))
318323
}
319324

320-
// TODO: Emit notification to UI
321-
_ = msg
322-
_ = isErr
325+
if e.notifyFunc != nil {
326+
e.notifyFunc(msg, isErr)
327+
}
323328

324329
return 0
325330
}

0 commit comments

Comments
 (0)