Add stdio MCP server for token-scoped ticket access.
Expose tm mcp tools against the local daemon, document agent setup, and ignore workspace .tm_token files. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"tm": {
|
||||||
|
"command": "${workspaceFolder}/bin/tm",
|
||||||
|
"args": ["mcp"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
/bin/
|
/bin/
|
||||||
.dev-home/
|
.dev-home/
|
||||||
|
.tm_token
|
||||||
|
**/.tm_token
|
||||||
|
|
||||||
*.exe
|
*.exe
|
||||||
*.test
|
*.test
|
||||||
*.out
|
*.out
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Agent notes — task-master
|
||||||
|
|
||||||
|
## Tickets über MCP
|
||||||
|
|
||||||
|
Tickets lesen und bearbeiten nur über den lokalen MCP-Server von task-master — nicht über JSON-Dateien oder die TUI.
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
1. Binary: `./bin/tm` (oder `tm` im PATH)
|
||||||
|
2. Projekt-MCP: [`.cursor/mcp.json`](.cursor/mcp.json) (`tm` → `args: ["mcp"]`)
|
||||||
|
3. Im Repo (oder darüber) `.tm_token` mit dem Projekt, für das die TUI ein Token erzeugt hat (`p` → Projekt wählen → `g`):
|
||||||
|
|
||||||
|
```
|
||||||
|
project=<projekt-id>
|
||||||
|
token=tm_mcp_…
|
||||||
|
```
|
||||||
|
|
||||||
|
`.tm_token` nicht committen.
|
||||||
|
|
||||||
|
### Verhalten
|
||||||
|
|
||||||
|
- Ohne gültiges `.tm_token` darf der MCP-Server den Daemon nicht nutzen.
|
||||||
|
- Mit Token sind alle Tools auf genau dieses eine TM-Projekt beschränkt.
|
||||||
|
- TUI/`tm sync` brauchen kein Token; die KI schon.
|
||||||
|
|
||||||
|
### Typische Tools
|
||||||
|
|
||||||
|
`list_projects`, `list_tickets`, `get_ticket`, `create_ticket`, `update_ticket`, `move_ticket`, `delete_ticket`, `list_time`, `add_time`, `undo`, `redo`, `sync_pull`, `sync_push`
|
||||||
|
|
||||||
|
Mehr Details: [README.md](README.md) (Abschnitt MCP).
|
||||||
@@ -66,7 +66,23 @@ tm help
|
|||||||
|
|
||||||
`tm` speichert weiterhin JSON unter dem DataDir, schreibt aber nur noch über einen lokalen Daemon-Prozess (eine Instanz pro DataDir). Socket/PID liegen unter `$XDG_RUNTIME_DIR/task-master-<hash>/` (Fallback: Temp-Dir). Die TUI und `tm sync` verbinden sich als vertrauenswürdige Clients (`X-TM-Client: tui|cli`).
|
`tm` speichert weiterhin JSON unter dem DataDir, schreibt aber nur noch über einen lokalen Daemon-Prozess (eine Instanz pro DataDir). Socket/PID liegen unter `$XDG_RUNTIME_DIR/task-master-<hash>/` (Fallback: Temp-Dir). Die TUI und `tm sync` verbinden sich als vertrauenswürdige Clients (`X-TM-Client: tui|cli`).
|
||||||
|
|
||||||
MCP-Token: in der Projektliste `g` erzeugt ein Token (Klartext einmal in der Statuszeile); `G` widerruft. Speichere es als `.tm_token` im Repo für den MCP-Server (`tm mcp`, folgt / siehe Help).
|
### MCP (`tm mcp`)
|
||||||
|
|
||||||
|
1. In der TUI-Projektliste `g` → Token erzeugen (Klartext einmal in der Statuszeile); `G` widerruft.
|
||||||
|
2. Im Repo-Root (oder darüber) `.tm_token` anlegen — **nicht committen**:
|
||||||
|
|
||||||
|
```
|
||||||
|
project=inbox
|
||||||
|
token=tm_mcp_…
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Cursor / MCP-Client:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "command": "tm", "args": ["mcp"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
Ohne gültiges `.tm_token` macht der MCP-Server keine Daemon-Aufrufe. Mit Token sind Lesen und Schreiben nur für dieses eine TM-Projekt erlaubt.
|
||||||
|
|
||||||
### Web-Server (`tm_server`)
|
### Web-Server (`tm_server`)
|
||||||
|
|
||||||
|
|||||||
+25
-2
@@ -12,6 +12,7 @@ import (
|
|||||||
"task/internal/daemon"
|
"task/internal/daemon"
|
||||||
"task/internal/daemon/api"
|
"task/internal/daemon/api"
|
||||||
"task/internal/daemon/client"
|
"task/internal/daemon/client"
|
||||||
|
mcpserver "task/internal/mcp"
|
||||||
"task/internal/ui"
|
"task/internal/ui"
|
||||||
"task/internal/version"
|
"task/internal/version"
|
||||||
)
|
)
|
||||||
@@ -215,7 +216,29 @@ func runSync(args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runMCP() error {
|
func runMCP() error {
|
||||||
return fmt.Errorf("mcp server not implemented yet — coming in phase 2")
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tf, err := mcpserver.FindTokenFile(cwd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cfg, err := loadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
paths, err := daemon.Ensure(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("daemon: %w", err)
|
||||||
|
}
|
||||||
|
cli := client.Dial(client.Options{
|
||||||
|
Socket: paths.Socket,
|
||||||
|
Kind: api.ClientMCP,
|
||||||
|
Token: tf.Token,
|
||||||
|
Project: tf.Project,
|
||||||
|
})
|
||||||
|
return mcpserver.ServeStdio(cli, tf)
|
||||||
}
|
}
|
||||||
|
|
||||||
func printHelp() {
|
func printHelp() {
|
||||||
@@ -238,7 +261,7 @@ Env:
|
|||||||
|
|
||||||
The TUI talks to a per-DataDir background daemon over a Unix socket so multiple
|
The TUI talks to a per-DataDir background daemon over a Unix socket so multiple
|
||||||
instances stay consistent. Create an MCP token from the project list (key g);
|
instances stay consistent. Create an MCP token from the project list (key g);
|
||||||
place it in a repo as .tm_token for AI access.
|
place project= and token= in a repo .tm_token file, then run: tm mcp
|
||||||
|
|
||||||
`, config.CliName, config.CliName, config.CliName, config.CliName, config.CliName, config.CliName, config.CliName, config.CliName, config.CliName, config.CliName)
|
`, config.CliName, config.CliName, config.CliName, config.CliName, config.CliName, config.CliName, config.CliName, config.CliName, config.CliName, config.CliName)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ require (
|
|||||||
github.com/charmbracelet/glamour v1.0.0
|
github.com/charmbracelet/glamour v1.0.0
|
||||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
|
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/mark3labs/mcp-go v0.32.0
|
||||||
github.com/muesli/termenv v0.16.0
|
github.com/muesli/termenv v0.16.0
|
||||||
golang.org/x/crypto v0.58.0
|
golang.org/x/crypto v0.58.0
|
||||||
modernc.org/sqlite v1.61.0
|
modernc.org/sqlite v1.61.0
|
||||||
@@ -42,7 +43,9 @@ require (
|
|||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/sahilm/fuzzy v0.1.1 // indirect
|
github.com/sahilm/fuzzy v0.1.1 // indirect
|
||||||
|
github.com/spf13/cast v1.7.1 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||||
github.com/yuin/goldmark v1.7.13 // indirect
|
github.com/yuin/goldmark v1.7.13 // indirect
|
||||||
github.com/yuin/goldmark-emoji v1.0.6 // indirect
|
github.com/yuin/goldmark-emoji v1.0.6 // indirect
|
||||||
golang.org/x/net v0.60.0 // indirect
|
golang.org/x/net v0.60.0 // indirect
|
||||||
|
|||||||
@@ -38,12 +38,18 @@ github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfa
|
|||||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||||
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
|
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
|
||||||
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/dustin/go-humanize v1.1.0 h1:dbKTrvD0klcbBV/h4AWJdMuZogJACoMlvWIWZ5b2xWg=
|
github.com/dustin/go-humanize v1.1.0 h1:dbKTrvD0klcbBV/h4AWJdMuZogJACoMlvWIWZ5b2xWg=
|
||||||
github.com/dustin/go-humanize v1.1.0/go.mod h1:hc1CvRkJMsgxqjmjMQF3QNRAZBwY8AXBAzKYoSX9sFI=
|
github.com/dustin/go-humanize v1.1.0/go.mod h1:hc1CvRkJMsgxqjmjMQF3QNRAZBwY8AXBAzKYoSX9sFI=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/pprof v0.0.0-20261016150608-1facf8f91b50 h1:PlNWJRxu5APSV0pWqfIsZ0qt9OOGHSWfZQ4zvx+WgWs=
|
github.com/google/pprof v0.0.0-20261016150608-1facf8f91b50 h1:PlNWJRxu5APSV0pWqfIsZ0qt9OOGHSWfZQ4zvx+WgWs=
|
||||||
github.com/google/pprof v0.0.0-20261016150608-1facf8f91b50/go.mod h1:yUo1AMMpsPIDFGuwDK8nLcf8+vvAd5l2O6JDZ6hpwrM=
|
github.com/google/pprof v0.0.0-20261016150608-1facf8f91b50/go.mod h1:yUo1AMMpsPIDFGuwDK8nLcf8+vvAd5l2O6JDZ6hpwrM=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
@@ -54,10 +60,16 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
|
|||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mark3labs/mcp-go v0.32.0 h1:fgwmbfL2gbd67obg57OfV2Dnrhs1HtSdlY/i5fn7MU8=
|
||||||
|
github.com/mark3labs/mcp-go v0.32.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
|
||||||
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||||
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
@@ -77,16 +89,26 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc
|
|||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
github.com/ncruces/go-strftime v1.1.0 h1:vtPmPWMkb539RGV9WpWT9EwJGvrOCKvMNI4obWL47Hk=
|
github.com/ncruces/go-strftime v1.1.0 h1:vtPmPWMkb539RGV9WpWT9EwJGvrOCKvMNI4obWL47Hk=
|
||||||
github.com/ncruces/go-strftime v1.1.0/go.mod h1:yVaq7iwCN09vHRrKBrrflXT0M9tTMG2KmZ9Fj7xeG/g=
|
github.com/ncruces/go-strftime v1.1.0/go.mod h1:yVaq7iwCN09vHRrKBrrflXT0M9tTMG2KmZ9Fj7xeG/g=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
|
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
|
||||||
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||||
|
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||||
|
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||||
|
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||||
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
|
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
|
||||||
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
|
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
|
||||||
@@ -110,6 +132,8 @@ golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
|
|||||||
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
|
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
|
||||||
golang.org/x/tools v0.52.0 h1:ryxg4U63oaUYk7uOLTF37n43f4qgtGvqV4Ioit1BaEs=
|
golang.org/x/tools v0.52.0 h1:ryxg4U63oaUYk7uOLTF37n43f4qgtGvqV4Ioit1BaEs=
|
||||||
golang.org/x/tools v0.52.0/go.mod h1:ItcP5UNaVGG+Yc4eIYmaFLNJ4soEopkx5Y2d4lcEurs=
|
golang.org/x/tools v0.52.0/go.mod h1:ItcP5UNaVGG+Yc4eIYmaFLNJ4soEopkx5Y2d4lcEurs=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
modernc.org/cc/v4 v4.30.3 h1:aDq8gIX1X7+VFgO/JYYji6pLWL2H4tv3g8WatT3KCys=
|
modernc.org/cc/v4 v4.30.3 h1:aDq8gIX1X7+VFgO/JYYji6pLWL2H4tv3g8WatT3KCys=
|
||||||
modernc.org/cc/v4 v4.30.3/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
modernc.org/cc/v4 v4.30.3/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||||
modernc.org/ccgo/v4 v4.37.0 h1:BAuJCUtIHIxFIYaFhqXIw5YaoMBlNCUlFovejHfMip4=
|
modernc.org/ccgo/v4 v4.37.0 h1:BAuJCUtIHIxFIYaFhqXIw5YaoMBlNCUlFovejHfMip4=
|
||||||
|
|||||||
@@ -0,0 +1,278 @@
|
|||||||
|
package mcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
|
||||||
|
"task/internal/daemon/client"
|
||||||
|
"task/internal/ticket"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServeStdio runs the MCP server on stdin/stdout using auth from .tm_token.
|
||||||
|
func ServeStdio(cli *client.Client, tf TokenFile) error {
|
||||||
|
s := server.NewMCPServer(
|
||||||
|
"task-master",
|
||||||
|
"1.0.0",
|
||||||
|
server.WithToolCapabilities(true),
|
||||||
|
)
|
||||||
|
registerTools(s, cli, tf)
|
||||||
|
return server.ServeStdio(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerTools(s *server.MCPServer, cli *client.Client, tf TokenFile) {
|
||||||
|
jsonResult := func(v any) (*mcp.CallToolResult, error) {
|
||||||
|
b, err := json.MarshalIndent(v, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError(err.Error()), nil
|
||||||
|
}
|
||||||
|
return mcp.NewToolResultText(string(b)), nil
|
||||||
|
}
|
||||||
|
fail := func(err error) (*mcp.CallToolResult, error) {
|
||||||
|
return mcp.NewToolResultError(err.Error()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s.AddTool(mcp.NewTool("list_projects",
|
||||||
|
mcp.WithDescription("List the TM project bound to .tm_token"),
|
||||||
|
), func(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
_ = ctx
|
||||||
|
list, err := cli.ProjectStore().List()
|
||||||
|
if err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
return jsonResult(list)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.AddTool(mcp.NewTool("list_tickets",
|
||||||
|
mcp.WithDescription("List tickets in the token project"),
|
||||||
|
mcp.WithString("status", mcp.Description("Optional status filter: todo, in_progress, done")),
|
||||||
|
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
_ = ctx
|
||||||
|
list, err := cli.TicketStore().List(tf.Project)
|
||||||
|
if err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
status := req.GetString("status", "")
|
||||||
|
if status != "" {
|
||||||
|
filtered := list[:0]
|
||||||
|
for _, t := range list {
|
||||||
|
if string(t.Status) == status {
|
||||||
|
filtered = append(filtered, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list = filtered
|
||||||
|
}
|
||||||
|
return jsonResult(list)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.AddTool(mcp.NewTool("get_ticket",
|
||||||
|
mcp.WithDescription("Get one ticket by id"),
|
||||||
|
mcp.WithString("id", mcp.Required(), mcp.Description("Ticket id")),
|
||||||
|
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
_ = ctx
|
||||||
|
id, err := req.RequireString("id")
|
||||||
|
if err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
t, err := cli.TicketStore().Get(id)
|
||||||
|
if err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
return jsonResult(t)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.AddTool(mcp.NewTool("create_ticket",
|
||||||
|
mcp.WithDescription("Create a ticket in the token project"),
|
||||||
|
mcp.WithString("title", mcp.Required(), mcp.Description("Ticket title")),
|
||||||
|
mcp.WithString("description", mcp.Description("Markdown body")),
|
||||||
|
mcp.WithString("status", mcp.Description("todo|in_progress|done")),
|
||||||
|
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
_ = ctx
|
||||||
|
title, err := req.RequireString("title")
|
||||||
|
if err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
st := ticket.Status(req.GetString("status", string(ticket.StatusTodo)))
|
||||||
|
t := ticket.Ticket{
|
||||||
|
Title: title,
|
||||||
|
Description: req.GetString("description", ""),
|
||||||
|
Status: st,
|
||||||
|
ProjectID: tf.Project,
|
||||||
|
}
|
||||||
|
created, err := cli.TicketStore().Create(t)
|
||||||
|
if err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
return jsonResult(created)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.AddTool(mcp.NewTool("update_ticket",
|
||||||
|
mcp.WithDescription("Update ticket title, description, and/or status"),
|
||||||
|
mcp.WithString("id", mcp.Required(), mcp.Description("Ticket id")),
|
||||||
|
mcp.WithString("title", mcp.Description("New title")),
|
||||||
|
mcp.WithString("description", mcp.Description("New description")),
|
||||||
|
mcp.WithString("status", mcp.Description("todo|in_progress|done")),
|
||||||
|
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
_ = ctx
|
||||||
|
id, err := req.RequireString("id")
|
||||||
|
if err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
before, err := cli.TicketStore().Get(id)
|
||||||
|
if err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
after := before
|
||||||
|
if v := req.GetString("title", ""); v != "" {
|
||||||
|
after.Title = v
|
||||||
|
}
|
||||||
|
if _, ok := req.GetArguments()["description"]; ok {
|
||||||
|
after.Description = req.GetString("description", "")
|
||||||
|
}
|
||||||
|
if v := req.GetString("status", ""); v != "" {
|
||||||
|
after.Status = ticket.Status(v)
|
||||||
|
}
|
||||||
|
if err := cli.TicketStore().Update(after); err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
return jsonResult(after)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.AddTool(mcp.NewTool("move_ticket",
|
||||||
|
mcp.WithDescription("Move a ticket (MCP only allows same project id)"),
|
||||||
|
mcp.WithString("id", mcp.Required()),
|
||||||
|
mcp.WithString("to_project_id", mcp.Required()),
|
||||||
|
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
_ = ctx
|
||||||
|
id, err := req.RequireString("id")
|
||||||
|
if err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
to, err := req.RequireString("to_project_id")
|
||||||
|
if err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
moved, err := cli.TicketStore().Move(id, to)
|
||||||
|
if err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
return jsonResult(moved)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.AddTool(mcp.NewTool("delete_ticket",
|
||||||
|
mcp.WithDescription("Delete a ticket by id"),
|
||||||
|
mcp.WithString("id", mcp.Required()),
|
||||||
|
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
_ = ctx
|
||||||
|
id, err := req.RequireString("id")
|
||||||
|
if err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
if err := cli.TicketStore().Delete(id); err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
return mcp.NewToolResultText("deleted"), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
s.AddTool(mcp.NewTool("list_time",
|
||||||
|
mcp.WithDescription("List time entries for the token project"),
|
||||||
|
mcp.WithString("ticket_id", mcp.Description("Optional ticket filter")),
|
||||||
|
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
_ = ctx
|
||||||
|
ticketID := req.GetString("ticket_id", "")
|
||||||
|
var list []ticket.TimeEntry
|
||||||
|
var err error
|
||||||
|
if ticketID != "" {
|
||||||
|
list, err = cli.TimeStore().ListByTicket(ticketID)
|
||||||
|
} else {
|
||||||
|
list, err = cli.TimeStore().List()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
return jsonResult(list)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.AddTool(mcp.NewTool("add_time",
|
||||||
|
mcp.WithDescription("Add a manual time entry"),
|
||||||
|
mcp.WithString("ticket_id", mcp.Required()),
|
||||||
|
mcp.WithNumber("minutes", mcp.Required(), mcp.Description("Duration in minutes")),
|
||||||
|
mcp.WithString("note", mcp.Description("Optional note")),
|
||||||
|
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
_ = ctx
|
||||||
|
ticketID, err := req.RequireString("ticket_id")
|
||||||
|
if err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
mins, err := req.RequireFloat("minutes")
|
||||||
|
if err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
tk, err := cli.TicketStore().Get(ticketID)
|
||||||
|
if err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
dur := time.Duration(mins * float64(time.Minute))
|
||||||
|
end := time.Now()
|
||||||
|
start := end.Add(-dur)
|
||||||
|
entry := ticket.TimeEntry{
|
||||||
|
TicketID: ticketID,
|
||||||
|
ProjectID: tk.ProjectID,
|
||||||
|
Start: start,
|
||||||
|
End: end,
|
||||||
|
Duration: dur,
|
||||||
|
Source: ticket.SourceManual,
|
||||||
|
Note: req.GetString("note", ""),
|
||||||
|
}
|
||||||
|
created, err := cli.TimeStore().Create(entry)
|
||||||
|
if err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
return jsonResult(created)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.AddTool(mcp.NewTool("undo",
|
||||||
|
mcp.WithDescription("Undo the last daemon command"),
|
||||||
|
), func(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
_ = ctx
|
||||||
|
if err := cli.Executor().Undo(); err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
return mcp.NewToolResultText("ok"), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
s.AddTool(mcp.NewTool("redo",
|
||||||
|
mcp.WithDescription("Redo the last undone daemon command"),
|
||||||
|
), func(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
_ = ctx
|
||||||
|
if err := cli.Executor().Redo(); err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
return mcp.NewToolResultText("ok"), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
s.AddTool(mcp.NewTool("sync_pull",
|
||||||
|
mcp.WithDescription("Pull tickets/time from the configured backend for the token project"),
|
||||||
|
), func(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
_ = ctx
|
||||||
|
res, err := cli.SyncPull(tf.Project)
|
||||||
|
if err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
return jsonResult(res)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.AddTool(mcp.NewTool("sync_push",
|
||||||
|
mcp.WithDescription("Push tickets/time to the configured backend for the token project"),
|
||||||
|
), func(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
_ = ctx
|
||||||
|
res, err := cli.SyncPush(tf.Project)
|
||||||
|
if err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
return jsonResult(res)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package mcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const tokenFileName = ".tm_token"
|
||||||
|
|
||||||
|
// TokenFile holds credentials from a workspace .tm_token file.
|
||||||
|
type TokenFile struct {
|
||||||
|
Project string
|
||||||
|
Token string
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindTokenFile walks from start (usually cwd) upward looking for .tm_token.
|
||||||
|
func FindTokenFile(start string) (TokenFile, error) {
|
||||||
|
dir, err := filepath.Abs(start)
|
||||||
|
if err != nil {
|
||||||
|
return TokenFile{}, err
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
path := filepath.Join(dir, tokenFileName)
|
||||||
|
if st, err := os.Stat(path); err == nil && !st.IsDir() {
|
||||||
|
tf, err := ParseTokenFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return TokenFile{}, err
|
||||||
|
}
|
||||||
|
return tf, nil
|
||||||
|
}
|
||||||
|
parent := filepath.Dir(dir)
|
||||||
|
if parent == dir {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
dir = parent
|
||||||
|
}
|
||||||
|
return TokenFile{}, fmt.Errorf("no %s found (create an MCP token in the TUI with g, then place project=… and token=… in %s)", tokenFileName, tokenFileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseTokenFile reads project= and token= lines from path.
|
||||||
|
func ParseTokenFile(path string) (TokenFile, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return TokenFile{}, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
tf := TokenFile{Path: path}
|
||||||
|
sc := bufio.NewScanner(f)
|
||||||
|
for sc.Scan() {
|
||||||
|
line := strings.TrimSpace(sc.Text())
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key, val, ok := strings.Cut(line, "=")
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key = strings.TrimSpace(strings.ToLower(key))
|
||||||
|
val = strings.TrimSpace(val)
|
||||||
|
switch key {
|
||||||
|
case "project":
|
||||||
|
tf.Project = val
|
||||||
|
case "token":
|
||||||
|
tf.Token = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := sc.Err(); err != nil {
|
||||||
|
return TokenFile{}, err
|
||||||
|
}
|
||||||
|
if tf.Project == "" || tf.Token == "" {
|
||||||
|
return TokenFile{}, fmt.Errorf("%s must contain project= and token= lines", path)
|
||||||
|
}
|
||||||
|
return tf, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package mcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseAndFindTokenFile(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
sub := filepath.Join(root, "a", "b")
|
||||||
|
if err := os.MkdirAll(sub, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
path := filepath.Join(root, ".tm_token")
|
||||||
|
content := "# comment\nproject=inbox\ntoken=tm_mcp_abc\n"
|
||||||
|
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tf, err := FindTokenFile(sub)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if tf.Project != "inbox" || tf.Token != "tm_mcp_abc" {
|
||||||
|
t.Fatalf("got %+v", tf)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = FindTokenFile(t.TempDir())
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected missing token error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseTokenFileIncomplete(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), ".tm_token")
|
||||||
|
if err := os.WriteFile(path, []byte("project=only\n"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := ParseTokenFile(path); err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user