on main help sign in
refactor(mcp): use the new mcp library (#1208)
ca66a11Carlos Alexandro Becker
go.mod+2−2
@@ -26,7 +26,7 @@ require (
26 github.com/google/uuid v1.6.0
26 github.com/google/uuid v1.6.0
27 github.com/invopop/jsonschema v0.13.0
27 github.com/invopop/jsonschema v0.13.0
28 github.com/joho/godotenv v1.5.1
28 github.com/joho/godotenv v1.5.1
29− github.com/mark3labs/mcp-go v0.41.1
29+ github.com/modelcontextprotocol/go-sdk v1.0.0
30 github.com/muesli/termenv v0.16.0
30 github.com/muesli/termenv v0.16.0
31 github.com/ncruces/go-sqlite3 v0.29.1
31 github.com/ncruces/go-sqlite3 v0.29.1
32 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
32 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
@@ -91,6 +91,7 @@ require (
91 github.com/go-logr/logr v1.4.3 // indirect
91 github.com/go-logr/logr v1.4.3 // indirect
92 github.com/go-logr/stdr v1.2.2 // indirect
92 github.com/go-logr/stdr v1.2.2 // indirect
93 github.com/google/go-cmp v0.7.0 // indirect
93 github.com/google/go-cmp v0.7.0 // indirect
94+ github.com/google/jsonschema-go v0.3.0 // indirect
94 github.com/google/s2a-go v0.1.8 // indirect
95 github.com/google/s2a-go v0.1.8 // indirect
95 github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
96 github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
96 github.com/googleapis/gax-go/v2 v2.14.1 // indirect
97 github.com/googleapis/gax-go/v2 v2.14.1 // indirect
@@ -121,7 +122,6 @@ require (
121 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
122 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
122 github.com/sethvargo/go-retry v0.3.0 // indirect
123 github.com/sethvargo/go-retry v0.3.0 // indirect
123 github.com/sourcegraph/jsonrpc2 v0.2.1 // indirect
124 github.com/sourcegraph/jsonrpc2 v0.2.1 // indirect
124− github.com/spf13/cast v1.7.1 // indirect
125 github.com/spf13/pflag v1.0.9 // indirect
125 github.com/spf13/pflag v1.0.9 // indirect
126 github.com/tetratelabs/wazero v1.9.0 // indirect
126 github.com/tetratelabs/wazero v1.9.0 // indirect
127 github.com/tidwall/gjson v1.18.0 // indirect
127 github.com/tidwall/gjson v1.18.0 // indirect
go.sum+8−8
@@ -130,8 +130,6 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
130github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
130github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
131github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
131github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
132github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
132github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
133−github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
134−github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
135github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
133github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
136github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
134github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
137github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
135github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
@@ -144,13 +142,15 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
144github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
142github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
145github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
143github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
146github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
144github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
147−github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
145+github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
148−github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
146+github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
149github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
147github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
150github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
148github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
151github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
149github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
152github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
150github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
153github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
151github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
152+github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
153+github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
154github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
154github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
155github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
155github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
156github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
156github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -194,8 +194,6 @@ github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQ
194github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
194github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
195github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
195github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
196github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
196github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
197−github.com/mark3labs/mcp-go v0.41.1 h1:w78eWfiQam2i8ICL7AL0WFiq7KHNJQ6UB53ZVtH4KGA=
198−github.com/mark3labs/mcp-go v0.41.1/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g=
199github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
197github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
200github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
198github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
201github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ=
199github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ=
@@ -206,6 +204,8 @@ github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwX
206github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
204github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
207github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
205github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
208github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
206github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
207+github.com/modelcontextprotocol/go-sdk v1.0.0 h1:Z4MSjLi38bTgLrd/LjSmofqRqyBiVKRyQSJgw8q8V74=
208+github.com/modelcontextprotocol/go-sdk v1.0.0/go.mod h1:nYtYQroQ2KQiM0/SbyEPUWQ6xs4B95gJjEalc9AQyOs=
209github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
209github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
210github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
210github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
211github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI=
211github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI=
@@ -265,8 +265,6 @@ github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah
265github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
265github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
266github.com/sourcegraph/jsonrpc2 v0.2.1 h1:2GtljixMQYUYCmIg7W9aF2dFmniq/mOr2T9tFRh6zSQ=
266github.com/sourcegraph/jsonrpc2 v0.2.1 h1:2GtljixMQYUYCmIg7W9aF2dFmniq/mOr2T9tFRh6zSQ=
267github.com/sourcegraph/jsonrpc2 v0.2.1/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo=
267github.com/sourcegraph/jsonrpc2 v0.2.1/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo=
268−github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
269−github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
270github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
268github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
271github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
269github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
272github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
270github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
@@ -422,6 +420,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
422golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
420golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
423golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
421golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
424golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
422golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
423+golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
424+golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
425golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
425golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
426google.golang.org/api v0.211.0 h1:IUpLjq09jxBSV1lACO33CGY3jsRcbctfGzhj+ZSE/Bg=
426google.golang.org/api v0.211.0 h1:IUpLjq09jxBSV1lACO33CGY3jsRcbctfGzhj+ZSE/Bg=
427google.golang.org/api v0.211.0/go.mod h1:XOloB4MXFH4UTlQSGuNUxw0UT74qdENK8d6JNsXKLi0=
427google.golang.org/api v0.211.0/go.mod h1:XOloB4MXFH4UTlQSGuNUxw0UT74qdENK8d6JNsXKLi0=
internal/config/config.go+1−1
@@ -99,7 +99,7 @@ type MCPType string
99
99
100const (
100const (
101 MCPStdio MCPType = "stdio"
101 MCPStdio MCPType = "stdio"
102− MCPSse MCPType = "sse"
102+ MCPSSE MCPType = "sse"
103 MCPHttp MCPType = "http"
103 MCPHttp MCPType = "http"
104)
104)
105
105
internal/llm/agent/agent.go+1
@@ -1,3 +1,4 @@
1+// Package agent contains the implementation of the AI agent service.
1package agent
2package agent
2
3
3import (
4import (
internal/llm/agent/mcp-tools.go+88−105
@@ -6,8 +6,11 @@ import (
6 "encoding/json"
6 "encoding/json"
7 "errors"
7 "errors"
8 "fmt"
8 "fmt"
9+ "io"
9 "log/slog"
10 "log/slog"
10 "maps"
11 "maps"
12+ "net/http"
13+ "os/exec"
11 "strings"
14 "strings"
12 "sync"
15 "sync"
13 "time"
16 "time"
@@ -19,9 +22,7 @@ import (
19 "github.com/charmbracelet/crush/internal/permission"
22 "github.com/charmbracelet/crush/internal/permission"
20 "github.com/charmbracelet/crush/internal/pubsub"
23 "github.com/charmbracelet/crush/internal/pubsub"
21 "github.com/charmbracelet/crush/internal/version"
24 "github.com/charmbracelet/crush/internal/version"
22− "github.com/mark3labs/mcp-go/client"
25+ "github.com/modelcontextprotocol/go-sdk/mcp"
23− "github.com/mark3labs/mcp-go/client/transport"
24− "github.com/mark3labs/mcp-go/mcp"
25)
26)
26
27
27// MCPState represents the current state of an MCP client
28// MCPState represents the current state of an MCP client
@@ -71,7 +72,7 @@ type MCPClientInfo struct {
71 Name string
72 Name string
72 State MCPState
73 State MCPState
73 Error error
74 Error error
74− Client *client.Client
75+ Client *mcp.ClientSession
75 ToolCount int
76 ToolCount int
76 ConnectedAt time.Time
77 ConnectedAt time.Time
77}
78}
@@ -80,14 +81,14 @@ var (
80 mcpToolsOnce sync.Once
81 mcpToolsOnce sync.Once
81 mcpTools = csync.NewMap[string, tools.BaseTool]()
82 mcpTools = csync.NewMap[string, tools.BaseTool]()
82 mcpClient2Tools = csync.NewMap[string, []tools.BaseTool]()
83 mcpClient2Tools = csync.NewMap[string, []tools.BaseTool]()
83− mcpClients = csync.NewMap[string, *client.Client]()
84+ mcpClients = csync.NewMap[string, *mcp.ClientSession]()
84 mcpStates = csync.NewMap[string, MCPClientInfo]()
85 mcpStates = csync.NewMap[string, MCPClientInfo]()
85 mcpBroker = pubsub.NewBroker[MCPEvent]()
86 mcpBroker = pubsub.NewBroker[MCPEvent]()
86)
87)
87
88
88type McpTool struct {
89type McpTool struct {
89 mcpName string
90 mcpName string
90− tool mcp.Tool
91+ tool *mcp.Tool
91 permissions permission.Service
92 permissions permission.Service
92 workingDir string
93 workingDir string
93}
94}
@@ -97,14 +98,9 @@ func (b *McpTool) Name() string {
97}
98}
98
99
99func (b *McpTool) Info() tools.ToolInfo {
100func (b *McpTool) Info() tools.ToolInfo {
100− required := b.tool.InputSchema.Required
101+ input := b.tool.InputSchema.(map[string]any)
101− if required == nil {
102− required = make([]string, 0)
102+ required, _ := input["required"].([]string)
103− }
103+ parameters, _ := input["properties"].(map[string]any)
104− parameters := b.tool.InputSchema.Properties
105− if parameters == nil {
106− parameters = make(map[string]any)
107− }
108 return tools.ToolInfo{
104 return tools.ToolInfo{
109 Name: fmt.Sprintf("mcp_%s_%s", b.mcpName, b.tool.Name),
105 Name: fmt.Sprintf("mcp_%s_%s", b.mcpName, b.tool.Name),
110 Description: b.tool.Description,
106 Description: b.tool.Description,
@@ -123,11 +119,9 @@ func runTool(ctx context.Context, name, toolName string, input string) (tools.To
123 if err != nil {
119 if err != nil {
124 return tools.NewTextErrorResponse(err.Error()), nil
120 return tools.NewTextErrorResponse(err.Error()), nil
125 }
121 }
126− result, err := c.CallTool(ctx, mcp.CallToolRequest{
127− Params: mcp.CallToolParams{
122+ result, err := c.CallTool(ctx, &mcp.CallToolParams{
128− Name: toolName,
123+ Name: toolName,
129− Arguments: args,
124+ Arguments: args,
130− },
131 })
125 })
132 if err != nil {
126 if err != nil {
133 return tools.NewTextErrorResponse(err.Error()), nil
127 return tools.NewTextErrorResponse(err.Error()), nil
@@ -135,8 +129,8 @@ func runTool(ctx context.Context, name, toolName string, input string) (tools.To
135
129
136 output := make([]string, 0, len(result.Content))
130 output := make([]string, 0, len(result.Content))
137 for _, v := range result.Content {
131 for _, v := range result.Content {
138− if v, ok := v.(mcp.TextContent); ok {
132+ if vv, ok := v.(*mcp.TextContent); ok {
139− output = append(output, v.Text)
133+ output = append(output, vv.Text)
140 } else {
134 } else {
141 output = append(output, fmt.Sprintf("%v", v))
135 output = append(output, fmt.Sprintf("%v", v))
142 }
136 }
@@ -144,8 +138,8 @@ func runTool(ctx context.Context, name, toolName string, input string) (tools.To
144 return tools.NewTextResponse(strings.Join(output, "\n")), nil
138 return tools.NewTextResponse(strings.Join(output, "\n")), nil
145}
139}
146
140
147−func getOrRenewClient(ctx context.Context, name string) (*client.Client, error) {
141+func getOrRenewClient(ctx context.Context, name string) (*mcp.ClientSession, error) {
148− c, ok := mcpClients.Get(name)
142+ sess, ok := mcpClients.Get(name)
149 if !ok {
143 if !ok {
150 return nil, fmt.Errorf("mcp '%s' not available", name)
144 return nil, fmt.Errorf("mcp '%s' not available", name)
151 }
145 }
@@ -157,20 +151,20 @@ func getOrRenewClient(ctx context.Context, name string) (*client.Client, error)
157 timeout := mcpTimeout(m)
151 timeout := mcpTimeout(m)
158 pingCtx, cancel := context.WithTimeout(ctx, timeout)
152 pingCtx, cancel := context.WithTimeout(ctx, timeout)
159 defer cancel()
153 defer cancel()
160− err := c.Ping(pingCtx)
154+ err := sess.Ping(pingCtx, nil)
161 if err == nil {
155 if err == nil {
162− return c, nil
156+ return sess, nil
163 }
157 }
164 updateMCPState(name, MCPStateError, maybeTimeoutErr(err, timeout), nil, state.ToolCount)
158 updateMCPState(name, MCPStateError, maybeTimeoutErr(err, timeout), nil, state.ToolCount)
165
159
166− c, err = createAndInitializeClient(ctx, name, m, cfg.Resolver())
160+ sess, err = createMCPSession(ctx, name, m, cfg.Resolver())
167 if err != nil {
161 if err != nil {
168 return nil, err
162 return nil, err
169 }
163 }
170
164
171− updateMCPState(name, MCPStateConnected, nil, c, state.ToolCount)
165+ updateMCPState(name, MCPStateConnected, nil, sess, state.ToolCount)
172− mcpClients.Set(name, c)
166+ mcpClients.Set(name, sess)
173− return c, nil
167+ return sess, nil
174}
168}
175
169
176func (b *McpTool) Run(ctx context.Context, params tools.ToolCall) (tools.ToolResponse, error) {
170func (b *McpTool) Run(ctx context.Context, params tools.ToolCall) (tools.ToolResponse, error) {
@@ -197,8 +191,8 @@ func (b *McpTool) Run(ctx context.Context, params tools.ToolCall) (tools.ToolRes
197 return runTool(ctx, b.mcpName, b.tool.Name, params.Input)
191 return runTool(ctx, b.mcpName, b.tool.Name, params.Input)
198}
192}
199
193
200−func getTools(ctx context.Context, name string, permissions permission.Service, c *client.Client, workingDir string) ([]tools.BaseTool, error) {
194+func getTools(ctx context.Context, name string, permissions permission.Service, c *mcp.ClientSession, workingDir string) ([]tools.BaseTool, error) {
201− result, err := c.ListTools(ctx, mcp.ListToolsRequest{})
195+ result, err := c.ListTools(ctx, &mcp.ListToolsParams{})
202 if err != nil {
196 if err != nil {
203 return nil, err
197 return nil, err
204 }
198 }
@@ -230,7 +224,7 @@ func GetMCPState(name string) (MCPClientInfo, bool) {
230}
224}
231
225
232// updateMCPState updates the state of an MCP client and publishes an event
226// updateMCPState updates the state of an MCP client and publishes an event
233−func updateMCPState(name string, state MCPState, err error, client *client.Client, toolCount int) {
227+func updateMCPState(name string, state MCPState, err error, client *mcp.ClientSession, toolCount int) {
234 info := MCPClientInfo{
228 info := MCPClientInfo{
235 Name: name,
229 Name: name,
236 State: state,
230 State: state,
@@ -257,19 +251,14 @@ func updateMCPState(name string, state MCPState, err error, client *client.Clien
257 })
251 })
258}
252}
259
253
260−// publishMCPEventToolsListChanged publishes a tool list changed event
261−func publishMCPEventToolsListChanged(name string) {
262− mcpBroker.Publish(pubsub.UpdatedEvent, MCPEvent{
263− Type: MCPEventToolsListChanged,
264− Name: name,
265− })
266−}
267−
268// CloseMCPClients closes all MCP clients. This should be called during application shutdown.
254// CloseMCPClients closes all MCP clients. This should be called during application shutdown.
269func CloseMCPClients() error {
255func CloseMCPClients() error {
270 var errs []error
256 var errs []error
271 for name, c := range mcpClients.Seq2() {
257 for name, c := range mcpClients.Seq2() {
272− if err := c.Close(); err != nil {
258+ if err := c.Close(); err != nil &&
259+ !errors.Is(err, io.EOF) &&
260+ !errors.Is(err, context.Canceled) &&
261+ err.Error() != "signal: killed" {
273 errs = append(errs, fmt.Errorf("close mcp: %s: %w", name, err))
262 errs = append(errs, fmt.Errorf("close mcp: %s: %w", name, err))
274 }
263 }
275 }
264 }
@@ -277,16 +266,6 @@ func CloseMCPClients() error {
277 return errors.Join(errs...)
266 return errors.Join(errs...)
278}
267}
279
268
280−var mcpInitRequest = mcp.InitializeRequest{
281− Params: mcp.InitializeParams{
282− ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION,
283− ClientInfo: mcp.Implementation{
284− Name: "Crush",
285− Version: version.Version,
286− },
287− },
288−}
289−
290func doGetMCPTools(ctx context.Context, permissions permission.Service, cfg *config.Config) {
269func doGetMCPTools(ctx context.Context, permissions permission.Service, cfg *config.Config) {
291 var wg sync.WaitGroup
270 var wg sync.WaitGroup
292 // Initialize states for all configured MCPs
271 // Initialize states for all configured MCPs
@@ -322,7 +301,7 @@ func doGetMCPTools(ctx context.Context, permissions permission.Service, cfg *con
322 ctx, cancel := context.WithTimeout(ctx, mcpTimeout(m))
301 ctx, cancel := context.WithTimeout(ctx, mcpTimeout(m))
323 defer cancel()
302 defer cancel()
324
303
325− c, err := createAndInitializeClient(ctx, name, m, cfg.Resolver())
304+ c, err := createMCPSession(ctx, name, m, cfg.Resolver())
326 if err != nil {
305 if err != nil {
327 return
306 return
328 }
307 }
@@ -359,49 +338,46 @@ func updateMcpTools(mcpName string, tools []tools.BaseTool) {
359 }
338 }
360}
339}
361
340
362−func createAndInitializeClient(ctx context.Context, name string, m config.MCPConfig, resolver config.VariableResolver) (*client.Client, error) {
341+func createMCPSession(ctx context.Context, name string, m config.MCPConfig, resolver config.VariableResolver) (*mcp.ClientSession, error) {
363− c, err := createMcpClient(name, m, resolver)
342+ timeout := mcpTimeout(m)
343+ mcpCtx, cancel := context.WithCancel(ctx)
344+ cancelTimer := time.AfterFunc(timeout, cancel)
345+
346+ transport, err := createMCPTransport(mcpCtx, m, resolver)
364 if err != nil {
347 if err != nil {
365 updateMCPState(name, MCPStateError, err, nil, 0)
348 updateMCPState(name, MCPStateError, err, nil, 0)
366 slog.Error("error creating mcp client", "error", err, "name", name)
349 slog.Error("error creating mcp client", "error", err, "name", name)
367 return nil, err
350 return nil, err
368 }
351 }
369
352
370− c.OnNotification(func(n mcp.JSONRPCNotification) {
353+ client := mcp.NewClient(
371− slog.Debug("Received MCP notification", "name", name, "notification", n)
354+ &mcp.Implementation{
372− switch n.Method {
355+ Name: "crush",
373− case "notifications/tools/list_changed":
356+ Version: version.Version,
374− publishMCPEventToolsListChanged(name)
357+ Title: "Crush",
375− default:
358+ },
376− slog.Debug("Unhandled MCP notification", "name", name, "method", n.Method)
359+ &mcp.ClientOptions{
377− }
360+ ToolListChangedHandler: func(context.Context, *mcp.ToolListChangedRequest) {
361+ mcpBroker.Publish(pubsub.UpdatedEvent, MCPEvent{
362+ Type: MCPEventToolsListChanged,
363+ Name: name,
378− })
364+ })
379−
365+ },
380− // XXX: ideally we should be able to use context.WithTimeout here, but,
366+ KeepAlive: time.Minute * 10,
381− // the SSE MCP client will start failing once that context is canceled.
367+ },
382− timeout := mcpTimeout(m)
368+ )
383− mcpCtx, cancel := context.WithCancel(ctx)
384− cancelTimer := time.AfterFunc(timeout, cancel)
385
369
386− if err := c.Start(mcpCtx); err != nil {
370+ session, err := client.Connect(mcpCtx, transport, nil)
371+ if err != nil {
387 updateMCPState(name, MCPStateError, maybeTimeoutErr(err, timeout), nil, 0)
372 updateMCPState(name, MCPStateError, maybeTimeoutErr(err, timeout), nil, 0)
388 slog.Error("error starting mcp client", "error", err, "name", name)
373 slog.Error("error starting mcp client", "error", err, "name", name)
389− _ = c.Close()
390− cancel()
391− return nil, err
392− }
393−
394− if _, err := c.Initialize(mcpCtx, mcpInitRequest); err != nil {
395− updateMCPState(name, MCPStateError, maybeTimeoutErr(err, timeout), nil, 0)
396− slog.Error("error initializing mcp client", "error", err, "name", name)
397− _ = c.Close()
398 cancel()
374 cancel()
399 return nil, err
375 return nil, err
400 }
376 }
401
377
402 cancelTimer.Stop()
378 cancelTimer.Stop()
403 slog.Info("Initialized mcp client", "name", name)
379 slog.Info("Initialized mcp client", "name", name)
404− return c, nil
380+ return session, nil
405}
381}
406
382
407func maybeTimeoutErr(err error, timeout time.Duration) error {
383func maybeTimeoutErr(err error, timeout time.Duration) error {
@@ -411,7 +387,7 @@ func maybeTimeoutErr(err error, timeout time.Duration) error {
411 return err
387 return err
412}
388}
413
389
414−func createMcpClient(name string, m config.MCPConfig, resolver config.VariableResolver) (*client.Client, error) {
390+func createMCPTransport(ctx context.Context, m config.MCPConfig, resolver config.VariableResolver) (mcp.Transport, error) {
415 switch m.Type {
391 switch m.Type {
416 case config.MCPStdio:
392 case config.MCPStdio:
417 command, err := resolver.ResolveValue(m.Command)
393 command, err := resolver.ResolveValue(m.Command)
@@ -421,44 +397,51 @@ func createMcpClient(name string, m config.MCPConfig, resolver config.VariableRe
421 if strings.TrimSpace(command) == "" {
397 if strings.TrimSpace(command) == "" {
422 return nil, fmt.Errorf("mcp stdio config requires a non-empty 'command' field")
398 return nil, fmt.Errorf("mcp stdio config requires a non-empty 'command' field")
423 }
399 }
424− return client.NewStdioMCPClientWithOptions(
425− home.Long(command),
400+ cmd := exec.CommandContext(ctx, home.Long(command), m.Args...)
426− m.ResolvedEnv(),
401+ cmd.Env = m.ResolvedEnv()
427− m.Args,
402+ return &mcp.CommandTransport{
428− transport.WithCommandLogger(mcpLogger{name: name}),
403+ Command: cmd,
429− )
404+ }, nil
430 case config.MCPHttp:
405 case config.MCPHttp:
431 if strings.TrimSpace(m.URL) == "" {
406 if strings.TrimSpace(m.URL) == "" {
432 return nil, fmt.Errorf("mcp http config requires a non-empty 'url' field")
407 return nil, fmt.Errorf("mcp http config requires a non-empty 'url' field")
433 }
408 }
434− return client.NewStreamableHttpClient(
409+ client := &http.Client{
410+ Transport: &headerRoundTripper{
411+ headers: m.ResolvedHeaders(),
412+ },
413+ }
414+ return &mcp.StreamableClientTransport{
435− m.URL,
415+ Endpoint: m.URL,
436− transport.WithHTTPHeaders(m.ResolvedHeaders()),
416+ HTTPClient: client,
437− transport.WithHTTPLogger(mcpLogger{name: name}),
417+ }, nil
438− )
439− case config.MCPSse:
418+ case config.MCPSSE:
440 if strings.TrimSpace(m.URL) == "" {
419 if strings.TrimSpace(m.URL) == "" {
441 return nil, fmt.Errorf("mcp sse config requires a non-empty 'url' field")
420 return nil, fmt.Errorf("mcp sse config requires a non-empty 'url' field")
442 }
421 }
443− return client.NewSSEMCPClient(
422+ client := &http.Client{
423+ Transport: &headerRoundTripper{
424+ headers: m.ResolvedHeaders(),
425+ },
426+ }
427+ return &mcp.SSEClientTransport{
444− m.URL,
428+ Endpoint: m.URL,
445− client.WithHeaders(m.ResolvedHeaders()),
429+ HTTPClient: client,
446− transport.WithSSELogger(mcpLogger{name: name}),
430+ }, nil
447− )
448 default:
431 default:
449 return nil, fmt.Errorf("unsupported mcp type: %s", m.Type)
432 return nil, fmt.Errorf("unsupported mcp type: %s", m.Type)
450 }
433 }
451}
434}
452
435
453−// for MCP's clients.
436+type headerRoundTripper struct {
454−type mcpLogger struct{ name string }
437+ headers map[string]string
455−
456−func (l mcpLogger) Errorf(format string, v ...any) {
457− slog.Error(fmt.Sprintf(format, v...), "name", l.name)
458}
438}
459
439
460−func (l mcpLogger) Infof(format string, v ...any) {
440+func (rt headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
461− slog.Info(fmt.Sprintf(format, v...), "name", l.name)
441+ for k, v := range rt.headers {
442+ req.Header.Set(k, v)
443+ }
444+ return http.DefaultTransport.RoundTrip(req)
462}
445}
463
446
464func mcpTimeout(m config.MCPConfig) time.Duration {
447func mcpTimeout(m config.MCPConfig) time.Duration {