mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-14 03:30:26 +00:00
new builtin servers
This commit is contained in:
@@ -54,4 +54,30 @@ MCPHost supports a simplified configuration schema with three server types:
|
||||
### Configuration Files
|
||||
- Primary: `~/.mcphost.yml` or `~/.mcphost.json`
|
||||
- Legacy: `~/.mcp.yml` or `~/.mcp.json`
|
||||
- Custom location via `--config` flag
|
||||
- Custom location via `--config` flag
|
||||
|
||||
## Available Builtin Servers
|
||||
MCPHost includes several builtin MCP servers for common functionality:
|
||||
|
||||
### Filesystem Server (`fs`)
|
||||
- **Location**: `internal/builtin/registry.go` (registration), uses external `mcp-filesystem-server`
|
||||
- **Purpose**: Secure filesystem access with configurable allowed directories
|
||||
- **Options**: `allowed_directories` array (defaults to current working directory)
|
||||
|
||||
### Bash Server (`bash`)
|
||||
- **Location**: `internal/builtin/bash.go`
|
||||
- **Purpose**: Execute bash commands with security restrictions and timeout controls
|
||||
- **Features**: Banned commands list, configurable timeouts, output size limits
|
||||
- **Security**: Blocks network commands (curl, wget, etc.), 30KB output limit, 2-10 minute timeouts
|
||||
|
||||
### Todo Server (`todo`)
|
||||
- **Location**: `internal/builtin/todo.go`
|
||||
- **Purpose**: Manage ephemeral todo lists for task tracking during sessions
|
||||
- **Features**: In-memory storage, thread-safe operations, JSON-based API
|
||||
- **Tools**: `todowrite` (create/update todos), `todoread` (read current todos)
|
||||
|
||||
### Fetch Server (`fetch`)
|
||||
- **Location**: `internal/builtin/fetch.go`
|
||||
- **Purpose**: Fetch web content and convert to text, markdown, or HTML formats
|
||||
- **Features**: HTTP/HTTPS support, HTML parsing, markdown conversion, size limits
|
||||
- **Security**: 5MB response limit, configurable timeouts, localhost-aware HTTPS upgrade
|
||||
@@ -183,6 +183,40 @@ Each builtin server entry requires:
|
||||
**Available Builtin Servers:**
|
||||
- `fs` (filesystem): Secure filesystem access with configurable allowed directories
|
||||
- `allowed_directories`: Array of directory paths that the server can access (defaults to current working directory if not specified)
|
||||
- `bash`: Execute bash commands with security restrictions and timeout controls
|
||||
- No configuration options required
|
||||
- `todo`: Manage ephemeral todo lists for task tracking during sessions
|
||||
- No configuration options required (todos are stored in memory and reset on restart)
|
||||
- `fetch`: Fetch web content and convert to text, markdown, or HTML formats
|
||||
- No configuration options required
|
||||
|
||||
#### Builtin Server Examples
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"filesystem": {
|
||||
"type": "builtin",
|
||||
"name": "fs",
|
||||
"options": {
|
||||
"allowed_directories": ["/tmp", "/home/user/documents"]
|
||||
}
|
||||
},
|
||||
"bash-commands": {
|
||||
"type": "builtin",
|
||||
"name": "bash"
|
||||
},
|
||||
"task-manager": {
|
||||
"type": "builtin",
|
||||
"name": "todo"
|
||||
},
|
||||
"web-fetcher": {
|
||||
"type": "builtin",
|
||||
"name": "fetch"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tool Filtering
|
||||
|
||||
|
||||
@@ -27,7 +27,10 @@ require (
|
||||
cloud.google.com/go v0.116.0 // indirect
|
||||
cloud.google.com/go/auth v0.15.0 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
||||
github.com/JohannesKaufmann/html-to-markdown v1.6.0 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.10.3 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/anthropics/anthropic-sdk-go v0.2.0-alpha.8 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.33.0 // indirect
|
||||
@@ -107,9 +110,10 @@ require (
|
||||
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
|
||||
go.opentelemetry.io/otel v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/arch v0.12.0 // indirect
|
||||
|
||||
@@ -4,8 +4,13 @@ cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps=
|
||||
cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8=
|
||||
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
|
||||
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
||||
github.com/JohannesKaufmann/html-to-markdown v1.6.0 h1:04VXMiE50YYfCfLboJCLcgqF5x+rHJnb1ssNmqpLH/k=
|
||||
github.com/JohannesKaufmann/html-to-markdown v1.6.0/go.mod h1:NUI78lGg/a7vpEJTz/0uOcYMaibytE4BUOQS8k78yPQ=
|
||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
|
||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||
github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o=
|
||||
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
|
||||
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
@@ -13,6 +18,9 @@ github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46
|
||||
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/anthropics/anthropic-sdk-go v0.2.0-alpha.8 h1:ss/c/eeyILgoK2sMsTJdcdLdhY3wZSt//+nanM41B9w=
|
||||
github.com/anthropics/anthropic-sdk-go v0.2.0-alpha.8/go.mod h1:GJxtdOs9K4neo8Gg65CjJ7jNautmldGli5/OFNabOoo=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
@@ -101,6 +109,7 @@ github.com/cloudwego/eino-ext/libs/acl/openai v0.0.0-20250605072634-0f875e04269d
|
||||
github.com/cloudwego/eino-ext/libs/acl/openai v0.0.0-20250605072634-0f875e04269d/go.mod h1:XKWiwOSkHWN23yd14et8A9khSKSYACU0xu4caySDA5U=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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=
|
||||
@@ -149,6 +158,7 @@ github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@@ -249,6 +259,7 @@ github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0V
|
||||
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
|
||||
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -263,6 +274,9 @@ github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtIS
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||
github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
@@ -280,6 +294,8 @@ github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
@@ -291,6 +307,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
@@ -324,6 +341,7 @@ github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5
|
||||
github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA=
|
||||
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.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
@@ -333,16 +351,16 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
|
||||
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
@@ -352,29 +370,98 @@ go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTV
|
||||
golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
|
||||
golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genai v1.10.0 h1:ETP0Yksn5KUSEn5+ihMOnP3IqjZ+7Z4i0LjJslEXatI=
|
||||
google.golang.org/genai v1.10.0/go.mod h1:TyfOKRz/QyCaj6f/ZDt505x+YreXnY40l2I6k8TvgqY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c=
|
||||
@@ -385,6 +472,7 @@ google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9x
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/JohannesKaufmann/html-to-markdown"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
const (
|
||||
maxResponseSize = 5 * 1024 * 1024 // 5MB
|
||||
defaultFetchTimeout = 30 * time.Second
|
||||
maxFetchTimeout = 120 * time.Second
|
||||
)
|
||||
|
||||
// NewFetchServer creates a new fetch MCP server
|
||||
func NewFetchServer() (*server.MCPServer, error) {
|
||||
s := server.NewMCPServer("fetch-server", "1.0.0", server.WithToolCapabilities(true))
|
||||
|
||||
// Register the fetch tool
|
||||
fetchTool := mcp.NewTool("fetch",
|
||||
mcp.WithDescription(fetchDescription),
|
||||
mcp.WithString("url",
|
||||
mcp.Required(),
|
||||
mcp.Description("The URL to fetch content from"),
|
||||
),
|
||||
mcp.WithString("format",
|
||||
mcp.Required(),
|
||||
mcp.Enum("text", "markdown", "html"),
|
||||
mcp.Description("The format to return the content in (text, markdown, or html)"),
|
||||
),
|
||||
mcp.WithNumber("timeout",
|
||||
mcp.Description("Optional timeout in seconds (max 120)"),
|
||||
mcp.Min(0),
|
||||
mcp.Max(120),
|
||||
),
|
||||
)
|
||||
|
||||
s.AddTool(fetchTool, executeFetch)
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// executeFetch handles the fetch tool execution
|
||||
func executeFetch(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
// Extract parameters
|
||||
urlStr, err := request.RequireString("url")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError("url parameter is required and must be a string"), nil
|
||||
}
|
||||
|
||||
format, err := request.RequireString("format")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError("format parameter is required and must be a string"), nil
|
||||
}
|
||||
|
||||
// Validate format
|
||||
if format != "text" && format != "markdown" && format != "html" {
|
||||
return mcp.NewToolResultError("format must be 'text', 'markdown', or 'html'"), nil
|
||||
}
|
||||
|
||||
// Parse timeout (optional)
|
||||
timeout := defaultFetchTimeout
|
||||
if timeoutSec := request.GetFloat("timeout", 0); timeoutSec > 0 {
|
||||
timeoutDuration := time.Duration(timeoutSec) * time.Second
|
||||
if timeoutDuration > maxFetchTimeout {
|
||||
timeout = maxFetchTimeout
|
||||
} else {
|
||||
timeout = timeoutDuration
|
||||
}
|
||||
}
|
||||
|
||||
// Validate URL
|
||||
parsedURL, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("invalid URL: %v", err)), nil
|
||||
}
|
||||
|
||||
// Ensure URL has a scheme
|
||||
if parsedURL.Scheme == "" {
|
||||
urlStr = "https://" + urlStr
|
||||
parsedURL, err = url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("invalid URL after adding https: %v", err)), nil
|
||||
}
|
||||
}
|
||||
|
||||
// Only allow HTTP and HTTPS
|
||||
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
|
||||
return mcp.NewToolResultError("URL must use http:// or https://"), nil
|
||||
}
|
||||
|
||||
// Upgrade HTTP to HTTPS only for external URLs (not localhost/127.0.0.1)
|
||||
if parsedURL.Scheme == "http" && !isLocalhost(parsedURL.Host) {
|
||||
parsedURL.Scheme = "https"
|
||||
urlStr = parsedURL.String()
|
||||
}
|
||||
|
||||
// Create HTTP client with timeout
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
// Create request with context
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to create request: %v", err)), nil
|
||||
}
|
||||
|
||||
// Set headers to mimic a real browser
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
|
||||
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8")
|
||||
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
|
||||
|
||||
// Make the request
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("request failed: %v", err)), nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check status code
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("request failed with status code: %d", resp.StatusCode)), nil
|
||||
}
|
||||
|
||||
// Check content length
|
||||
if resp.ContentLength > maxResponseSize {
|
||||
return mcp.NewToolResultError("response too large (exceeds 5MB limit)"), nil
|
||||
}
|
||||
|
||||
// Read response body with size limit
|
||||
limitedReader := io.LimitReader(resp.Body, maxResponseSize+1)
|
||||
bodyBytes, err := io.ReadAll(limitedReader)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to read response: %v", err)), nil
|
||||
}
|
||||
|
||||
// Check if we exceeded the size limit
|
||||
if len(bodyBytes) > maxResponseSize {
|
||||
return mcp.NewToolResultError("response too large (exceeds 5MB limit)"), nil
|
||||
}
|
||||
|
||||
content := string(bodyBytes)
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if contentType == "" {
|
||||
contentType = "unknown"
|
||||
}
|
||||
|
||||
// Process content based on format
|
||||
var output string
|
||||
switch format {
|
||||
case "text":
|
||||
if strings.Contains(contentType, "text/html") {
|
||||
output, err = extractTextFromHTML(content)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to extract text from HTML: %v", err)), nil
|
||||
}
|
||||
} else {
|
||||
output = content
|
||||
}
|
||||
|
||||
case "markdown":
|
||||
if strings.Contains(contentType, "text/html") {
|
||||
output, err = convertHTMLToMarkdown(content)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("failed to convert HTML to markdown: %v", err)), nil
|
||||
}
|
||||
} else {
|
||||
output = "```\n" + content + "\n```"
|
||||
}
|
||||
|
||||
case "html":
|
||||
output = content
|
||||
|
||||
default:
|
||||
output = content
|
||||
}
|
||||
|
||||
// Create result with metadata
|
||||
title := fmt.Sprintf("%s (%s)", urlStr, contentType)
|
||||
result := mcp.NewToolResultText(output)
|
||||
result.Meta = map[string]any{
|
||||
"title": title,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// extractTextFromHTML extracts plain text from HTML content
|
||||
func extractTextFromHTML(htmlContent string) (string, error) {
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Remove script, style, and other non-content elements
|
||||
doc.Find("script, style, noscript, iframe, object, embed").Remove()
|
||||
|
||||
// Extract text content
|
||||
text := doc.Text()
|
||||
|
||||
// Clean up whitespace
|
||||
lines := strings.Split(text, "\n")
|
||||
var cleanLines []string
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed != "" {
|
||||
cleanLines = append(cleanLines, trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(cleanLines, "\n"), nil
|
||||
}
|
||||
|
||||
// convertHTMLToMarkdown converts HTML content to markdown
|
||||
func convertHTMLToMarkdown(htmlContent string) (string, error) {
|
||||
converter := md.NewConverter("", true, nil)
|
||||
|
||||
// Remove unwanted elements
|
||||
converter.Remove("script")
|
||||
converter.Remove("style")
|
||||
converter.Remove("meta")
|
||||
converter.Remove("link")
|
||||
|
||||
markdown, err := converter.ConvertString(htmlContent)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return markdown, nil
|
||||
}
|
||||
|
||||
// isLocalhost checks if the host is localhost or 127.0.0.1
|
||||
func isLocalhost(host string) bool {
|
||||
return strings.HasPrefix(host, "localhost") ||
|
||||
strings.HasPrefix(host, "127.0.0.1") ||
|
||||
strings.HasPrefix(host, "::1")
|
||||
}
|
||||
|
||||
const fetchDescription = `Fetches content from a specified URL and returns it in the requested format.
|
||||
|
||||
- Fetches content from a specified URL
|
||||
- Takes a URL and format as input
|
||||
- Fetches the URL content, converts HTML to markdown or text as requested
|
||||
- Returns the content in the specified format
|
||||
- Use this tool when you need to retrieve and analyze web content
|
||||
|
||||
Usage notes:
|
||||
- IMPORTANT: If an MCP-provided web fetch tool is available, prefer using that tool instead of this one, as it may have fewer restrictions. All MCP-provided tools start with "mcp__".
|
||||
- The URL must be a fully-formed valid URL
|
||||
- HTTP URLs will be automatically upgraded to HTTPS
|
||||
- This tool is read-only and does not modify any files
|
||||
- Results may be summarized if the content is very large (max 5MB)
|
||||
- Supports three output formats:
|
||||
- "text": Plain text extraction from HTML, or raw content for non-HTML
|
||||
- "markdown": HTML converted to markdown, or code-wrapped for non-HTML
|
||||
- "html": Raw HTML content
|
||||
- Timeout can be specified in seconds (default 30s, max 120s)`
|
||||
@@ -0,0 +1,365 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
func TestNewFetchServer(t *testing.T) {
|
||||
server, err := NewFetchServer()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create fetch server: %v", err)
|
||||
}
|
||||
|
||||
if server == nil {
|
||||
t.Fatal("Expected server to be non-nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchServerRegistry(t *testing.T) {
|
||||
registry := NewRegistry()
|
||||
|
||||
// Test that fetch server is registered
|
||||
servers := registry.ListServers()
|
||||
found := false
|
||||
for _, name := range servers {
|
||||
if name == "fetch" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Error("fetch server not found in registry")
|
||||
}
|
||||
|
||||
// Test creating fetch server through registry
|
||||
wrapper, err := registry.CreateServer("fetch", map[string]any{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create fetch server through registry: %v", err)
|
||||
}
|
||||
|
||||
if wrapper == nil {
|
||||
t.Fatal("Expected wrapper to be non-nil")
|
||||
}
|
||||
|
||||
if wrapper.GetServer() == nil {
|
||||
t.Fatal("Expected wrapped server to be non-nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchHTML(t *testing.T) {
|
||||
// Create a test HTTP server
|
||||
testHTML := `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Page</title>
|
||||
<script>console.log('test');</script>
|
||||
<style>body { color: red; }</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World</h1>
|
||||
<p>This is a test paragraph.</p>
|
||||
<script>alert('should be removed');</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(testHTML))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Test HTML format
|
||||
request := mcp.CallToolRequest{
|
||||
Params: mcp.CallToolParams{
|
||||
Name: "fetch",
|
||||
Arguments: map[string]any{
|
||||
"url": server.URL,
|
||||
"format": "html",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := executeFetch(ctx, request)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to execute fetch: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("Expected result to be non-nil")
|
||||
}
|
||||
|
||||
if len(result.Content) == 0 {
|
||||
t.Fatal("Expected result to have content")
|
||||
}
|
||||
|
||||
// Check that we got HTML content
|
||||
if textContent, ok := mcp.AsTextContent(result.Content[0]); ok {
|
||||
if textContent.Text == "" {
|
||||
t.Error("Expected non-empty HTML content")
|
||||
}
|
||||
// Should contain the original HTML
|
||||
if !strings.Contains(textContent.Text, "<h1>Hello World</h1>") {
|
||||
t.Error("Expected HTML content to contain original markup")
|
||||
}
|
||||
} else {
|
||||
t.Error("Expected text content")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchText(t *testing.T) {
|
||||
// Create a test HTTP server
|
||||
testHTML := `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Page</title>
|
||||
<script>console.log('test');</script>
|
||||
<style>body { color: red; }</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World</h1>
|
||||
<p>This is a test paragraph.</p>
|
||||
<script>alert('should be removed');</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(testHTML))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Test text format
|
||||
request := mcp.CallToolRequest{
|
||||
Params: mcp.CallToolParams{
|
||||
Name: "fetch",
|
||||
Arguments: map[string]any{
|
||||
"url": server.URL,
|
||||
"format": "text",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := executeFetch(ctx, request)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to execute fetch: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("Expected result to be non-nil")
|
||||
}
|
||||
|
||||
if len(result.Content) == 0 {
|
||||
t.Fatal("Expected result to have content")
|
||||
}
|
||||
|
||||
// Check that we got text content
|
||||
if textContent, ok := mcp.AsTextContent(result.Content[0]); ok {
|
||||
if textContent.Text == "" {
|
||||
t.Error("Expected non-empty text content")
|
||||
}
|
||||
// Should contain the text but not HTML tags
|
||||
if !strings.Contains(textContent.Text, "Hello World") {
|
||||
t.Error("Expected text content to contain 'Hello World'")
|
||||
}
|
||||
if strings.Contains(textContent.Text, "<h1>") {
|
||||
t.Error("Expected text content to not contain HTML tags")
|
||||
}
|
||||
if strings.Contains(textContent.Text, "console.log") {
|
||||
t.Error("Expected text content to not contain script content")
|
||||
}
|
||||
} else {
|
||||
t.Error("Expected text content")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchMarkdown(t *testing.T) {
|
||||
// Create a test HTTP server
|
||||
testHTML := `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World</h1>
|
||||
<p>This is a test paragraph.</p>
|
||||
<ul>
|
||||
<li>Item 1</li>
|
||||
<li>Item 2</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(testHTML))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Test markdown format
|
||||
request := mcp.CallToolRequest{
|
||||
Params: mcp.CallToolParams{
|
||||
Name: "fetch",
|
||||
Arguments: map[string]any{
|
||||
"url": server.URL,
|
||||
"format": "markdown",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := executeFetch(ctx, request)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to execute fetch: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("Expected result to be non-nil")
|
||||
}
|
||||
|
||||
if len(result.Content) == 0 {
|
||||
t.Fatal("Expected result to have content")
|
||||
}
|
||||
|
||||
// Check that we got markdown content
|
||||
if textContent, ok := mcp.AsTextContent(result.Content[0]); ok {
|
||||
if textContent.Text == "" {
|
||||
t.Error("Expected non-empty markdown content")
|
||||
}
|
||||
// Should contain markdown formatting
|
||||
if !strings.Contains(textContent.Text, "# Hello World") {
|
||||
t.Error("Expected markdown content to contain '# Hello World'")
|
||||
}
|
||||
} else {
|
||||
t.Error("Expected text content")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchInvalidURL(t *testing.T) {
|
||||
request := mcp.CallToolRequest{
|
||||
Params: mcp.CallToolParams{
|
||||
Name: "fetch",
|
||||
Arguments: map[string]any{
|
||||
"url": "not-a-valid-url",
|
||||
"format": "text",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := executeFetch(ctx, request)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Should return an error result
|
||||
if result == nil {
|
||||
t.Fatal("Expected result to be non-nil")
|
||||
}
|
||||
|
||||
if len(result.Content) == 0 {
|
||||
t.Fatal("Expected result to have content")
|
||||
}
|
||||
|
||||
if textContent, ok := mcp.AsTextContent(result.Content[0]); ok {
|
||||
if textContent.Text == "" {
|
||||
t.Error("Expected non-empty error message")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchInvalidFormat(t *testing.T) {
|
||||
request := mcp.CallToolRequest{
|
||||
Params: mcp.CallToolParams{
|
||||
Name: "fetch",
|
||||
Arguments: map[string]any{
|
||||
"url": "https://example.com",
|
||||
"format": "invalid",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := executeFetch(ctx, request)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Should return an error result
|
||||
if result == nil {
|
||||
t.Fatal("Expected result to be non-nil")
|
||||
}
|
||||
|
||||
if len(result.Content) == 0 {
|
||||
t.Fatal("Expected result to have content")
|
||||
}
|
||||
|
||||
if textContent, ok := mcp.AsTextContent(result.Content[0]); ok {
|
||||
if textContent.Text == "" {
|
||||
t.Error("Expected non-empty error message")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchPlainText(t *testing.T) {
|
||||
// Create a test HTTP server that returns plain text
|
||||
testText := "This is plain text content.\nWith multiple lines.\nAnd some more text."
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(testText))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Test text format with plain text content
|
||||
request := mcp.CallToolRequest{
|
||||
Params: mcp.CallToolParams{
|
||||
Name: "fetch",
|
||||
Arguments: map[string]any{
|
||||
"url": server.URL,
|
||||
"format": "text",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := executeFetch(ctx, request)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to execute fetch: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("Expected result to be non-nil")
|
||||
}
|
||||
|
||||
if len(result.Content) == 0 {
|
||||
t.Fatal("Expected result to have content")
|
||||
}
|
||||
|
||||
// Check that we got the original text content
|
||||
if textContent, ok := mcp.AsTextContent(result.Content[0]); ok {
|
||||
if textContent.Text != testText {
|
||||
t.Errorf("Expected '%s', got '%s'", testText, textContent.Text)
|
||||
}
|
||||
} else {
|
||||
t.Error("Expected text content")
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,8 @@ func NewRegistry() *Registry {
|
||||
// Register builtin servers
|
||||
r.registerFilesystemServer()
|
||||
r.registerBashServer()
|
||||
r.registerTodoServer()
|
||||
r.registerFetchServer()
|
||||
|
||||
return r
|
||||
}
|
||||
@@ -115,3 +117,29 @@ func (r *Registry) registerBashServer() {
|
||||
return &BuiltinServerWrapper{server: server}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// registerTodoServer registers the todo server
|
||||
func (r *Registry) registerTodoServer() {
|
||||
r.servers["todo"] = func(options map[string]any) (*BuiltinServerWrapper, error) {
|
||||
// Create the todo server
|
||||
server, err := NewTodoServer()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create todo server: %v", err)
|
||||
}
|
||||
|
||||
return &BuiltinServerWrapper{server: server}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// registerFetchServer registers the fetch server
|
||||
func (r *Registry) registerFetchServer() {
|
||||
r.servers["fetch"] = func(options map[string]any) (*BuiltinServerWrapper, error) {
|
||||
// Create the fetch server
|
||||
server, err := NewFetchServer()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create fetch server: %v", err)
|
||||
}
|
||||
|
||||
return &BuiltinServerWrapper{server: server}, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
// TodoInfo represents a single todo item
|
||||
type TodoInfo struct {
|
||||
Content string `json:"content"`
|
||||
Status string `json:"status"`
|
||||
Priority string `json:"priority"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
// TodoServer implements a todo management MCP server with in-memory storage
|
||||
type TodoServer struct {
|
||||
todos []TodoInfo
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewTodoServer creates a new todo MCP server with in-memory storage
|
||||
func NewTodoServer() (*server.MCPServer, error) {
|
||||
todoServer := &TodoServer{
|
||||
todos: make([]TodoInfo, 0),
|
||||
}
|
||||
|
||||
s := server.NewMCPServer("todo-server", "1.0.0", server.WithToolCapabilities(true))
|
||||
|
||||
// Register todowrite tool
|
||||
todoWriteTool := mcp.NewTool("todowrite",
|
||||
mcp.WithDescription(todoWriteDescription),
|
||||
mcp.WithArray("todos",
|
||||
mcp.Required(),
|
||||
mcp.Description("The updated todo list"),
|
||||
mcp.Items(map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"content": map[string]any{
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Brief description of the task",
|
||||
},
|
||||
"status": map[string]any{
|
||||
"type": "string",
|
||||
"enum": []string{"pending", "in_progress", "completed"},
|
||||
"description": "Current status of the task",
|
||||
},
|
||||
"priority": map[string]any{
|
||||
"type": "string",
|
||||
"enum": []string{"high", "medium", "low"},
|
||||
"description": "Priority level of the task",
|
||||
},
|
||||
"id": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Unique identifier for the todo item",
|
||||
},
|
||||
},
|
||||
"required": []string{"content", "status", "priority", "id"},
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
// Register todoread tool
|
||||
todoReadTool := mcp.NewTool("todoread",
|
||||
mcp.WithDescription("Use this tool to read your todo list"),
|
||||
)
|
||||
|
||||
s.AddTool(todoWriteTool, todoServer.executeTodoWrite)
|
||||
s.AddTool(todoReadTool, todoServer.executeTodoRead)
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// getTodos retrieves all todos from memory
|
||||
func (ts *TodoServer) getTodos() []TodoInfo {
|
||||
ts.mutex.RLock()
|
||||
defer ts.mutex.RUnlock()
|
||||
|
||||
// Return a copy to avoid race conditions
|
||||
todos := make([]TodoInfo, len(ts.todos))
|
||||
copy(todos, ts.todos)
|
||||
return todos
|
||||
}
|
||||
|
||||
// setTodos stores todos in memory
|
||||
func (ts *TodoServer) setTodos(todos []TodoInfo) {
|
||||
ts.mutex.Lock()
|
||||
defer ts.mutex.Unlock()
|
||||
|
||||
ts.todos = make([]TodoInfo, len(todos))
|
||||
copy(ts.todos, todos)
|
||||
}
|
||||
|
||||
// executeTodoWrite handles the todowrite tool execution
|
||||
func (ts *TodoServer) executeTodoWrite(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
// Parse todos from arguments
|
||||
todosArg := request.GetArguments()["todos"]
|
||||
if todosArg == nil {
|
||||
return mcp.NewToolResultError("todos parameter is required"), nil
|
||||
}
|
||||
|
||||
// Convert to JSON and back to ensure proper structure
|
||||
todosJSON, err := json.Marshal(todosArg)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError("invalid todos format"), nil
|
||||
}
|
||||
|
||||
var todos []TodoInfo
|
||||
if err := json.Unmarshal(todosJSON, &todos); err != nil {
|
||||
return mcp.NewToolResultError("invalid todos structure"), nil
|
||||
}
|
||||
|
||||
// Validate todos
|
||||
for i, todo := range todos {
|
||||
if strings.TrimSpace(todo.Content) == "" {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("todo %d: content cannot be empty", i)), nil
|
||||
}
|
||||
if todo.Status != "pending" && todo.Status != "in_progress" && todo.Status != "completed" {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("todo %d: invalid status '%s'", i, todo.Status)), nil
|
||||
}
|
||||
if todo.Priority != "high" && todo.Priority != "medium" && todo.Priority != "low" {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("todo %d: invalid priority '%s'", i, todo.Priority)), nil
|
||||
}
|
||||
if strings.TrimSpace(todo.ID) == "" {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("todo %d: id cannot be empty", i)), nil
|
||||
}
|
||||
}
|
||||
|
||||
// Store todos in memory
|
||||
ts.setTodos(todos)
|
||||
|
||||
// Count non-completed todos
|
||||
activeTodos := 0
|
||||
for _, todo := range todos {
|
||||
if todo.Status != "completed" {
|
||||
activeTodos++
|
||||
}
|
||||
}
|
||||
|
||||
// Format output
|
||||
output, err := json.MarshalIndent(todos, "", " ")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError("failed to format todos"), nil
|
||||
}
|
||||
|
||||
// Create result with metadata
|
||||
result := mcp.NewToolResultText(string(output))
|
||||
result.Meta = map[string]any{
|
||||
"title": fmt.Sprintf("%d todos", activeTodos),
|
||||
"todos": todos,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// executeTodoRead handles the todoread tool execution
|
||||
func (ts *TodoServer) executeTodoRead(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
// Get todos from memory
|
||||
todos := ts.getTodos()
|
||||
|
||||
// Count non-completed todos
|
||||
activeTodos := 0
|
||||
for _, todo := range todos {
|
||||
if todo.Status != "completed" {
|
||||
activeTodos++
|
||||
}
|
||||
}
|
||||
|
||||
// Format output
|
||||
output, err := json.MarshalIndent(todos, "", " ")
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError("failed to format todos"), nil
|
||||
}
|
||||
|
||||
// Create result with metadata
|
||||
result := mcp.NewToolResultText(string(output))
|
||||
result.Meta = map[string]any{
|
||||
"title": fmt.Sprintf("%d todos", activeTodos),
|
||||
"todos": todos,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
const todoWriteDescription = `Use this tool to create and manage a structured task list for your current coding session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user.
|
||||
It also helps the user understand the progress of the task and overall progress of their requests.
|
||||
|
||||
## When to Use This Tool
|
||||
Use this tool proactively in these scenarios:
|
||||
|
||||
1. Complex multi-step tasks - When a task requires 3 or more distinct steps or actions
|
||||
2. Non-trivial and complex tasks - Tasks that require careful planning or multiple operations
|
||||
3. User explicitly requests todo list - When the user directly asks you to use the todo list
|
||||
4. User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated)
|
||||
5. After receiving new instructions - Immediately capture user requirements as todos. Feel free to edit the todo list based on new information.
|
||||
6. After completing a task - Mark it complete and add any new follow-up tasks
|
||||
7. When you start working on a new task, mark the todo as in_progress. Ideally you should only have one todo as in_progress at a time. Complete existing tasks before starting new ones.
|
||||
|
||||
## When NOT to Use This Tool
|
||||
|
||||
Skip using this tool when:
|
||||
1. There is only a single, straightforward task
|
||||
2. The task is trivial and tracking it provides no organizational benefit
|
||||
3. The task can be completed in less than 3 trivial steps
|
||||
4. The task is purely conversational or informational
|
||||
|
||||
NOTE that you should not use this tool if there is only one trivial task to do. In this case you are better off just doing the task directly.
|
||||
|
||||
## Task States and Management
|
||||
|
||||
1. **Task States**: Use these states to track progress:
|
||||
- pending: Task not yet started
|
||||
- in_progress: Currently working on (limit to ONE task at a time)
|
||||
- completed: Task finished successfully
|
||||
|
||||
2. **Task Management**:
|
||||
- Update task status in real-time as you work
|
||||
- Mark tasks complete IMMEDIATELY after finishing (don't batch completions)
|
||||
- Only have ONE task in_progress at any time
|
||||
- Complete current tasks before starting new ones
|
||||
|
||||
3. **Task Breakdown**:
|
||||
- Create specific, actionable items
|
||||
- Break complex tasks into smaller, manageable steps
|
||||
- Use clear, descriptive task names
|
||||
|
||||
When in doubt, use this tool. Being proactive with task management demonstrates attentiveness and ensures you complete all requirements successfully.`
|
||||
@@ -0,0 +1,298 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
func TestNewTodoServer(t *testing.T) {
|
||||
server, err := NewTodoServer()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create todo server: %v", err)
|
||||
}
|
||||
|
||||
if server == nil {
|
||||
t.Fatal("Expected server to be non-nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTodoServerRegistry(t *testing.T) {
|
||||
registry := NewRegistry()
|
||||
|
||||
// Test that todo server is registered
|
||||
servers := registry.ListServers()
|
||||
found := false
|
||||
for _, name := range servers {
|
||||
if name == "todo" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Error("todo server not found in registry")
|
||||
}
|
||||
|
||||
// Test creating todo server through registry
|
||||
wrapper, err := registry.CreateServer("todo", map[string]any{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create todo server through registry: %v", err)
|
||||
}
|
||||
|
||||
if wrapper == nil {
|
||||
t.Fatal("Expected wrapper to be non-nil")
|
||||
}
|
||||
|
||||
if wrapper.GetServer() == nil {
|
||||
t.Fatal("Expected wrapped server to be non-nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTodoWrite(t *testing.T) {
|
||||
server := &TodoServer{
|
||||
todos: make([]TodoInfo, 0),
|
||||
}
|
||||
|
||||
// Create a test request with valid todos
|
||||
todos := []TodoInfo{
|
||||
{
|
||||
Content: "Test task 1",
|
||||
Status: "pending",
|
||||
Priority: "high",
|
||||
ID: "1",
|
||||
},
|
||||
{
|
||||
Content: "Test task 2",
|
||||
Status: "in_progress",
|
||||
Priority: "medium",
|
||||
ID: "2",
|
||||
},
|
||||
}
|
||||
|
||||
request := mcp.CallToolRequest{
|
||||
Params: mcp.CallToolParams{
|
||||
Name: "todowrite",
|
||||
Arguments: map[string]any{
|
||||
"todos": todos,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := server.executeTodoWrite(ctx, request)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to execute todowrite: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("Expected result to be non-nil")
|
||||
}
|
||||
|
||||
if len(result.Content) == 0 {
|
||||
t.Fatal("Expected result to have content")
|
||||
}
|
||||
|
||||
// Check that todos were stored
|
||||
storedTodos := server.getTodos()
|
||||
if len(storedTodos) != 2 {
|
||||
t.Errorf("Expected 2 todos, got %d", len(storedTodos))
|
||||
}
|
||||
|
||||
// Verify the content
|
||||
if textContent, ok := mcp.AsTextContent(result.Content[0]); ok {
|
||||
var resultTodos []TodoInfo
|
||||
if err := json.Unmarshal([]byte(textContent.Text), &resultTodos); err != nil {
|
||||
t.Errorf("Failed to parse result JSON: %v", err)
|
||||
} else if len(resultTodos) != 2 {
|
||||
t.Errorf("Expected 2 todos in result, got %d", len(resultTodos))
|
||||
}
|
||||
} else {
|
||||
t.Error("Expected text content")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTodoRead(t *testing.T) {
|
||||
server := &TodoServer{
|
||||
todos: []TodoInfo{
|
||||
{
|
||||
Content: "Existing task",
|
||||
Status: "pending",
|
||||
Priority: "low",
|
||||
ID: "existing-1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
request := mcp.CallToolRequest{
|
||||
Params: mcp.CallToolParams{
|
||||
Name: "todoread",
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := server.executeTodoRead(ctx, request)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to execute todoread: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("Expected result to be non-nil")
|
||||
}
|
||||
|
||||
if len(result.Content) == 0 {
|
||||
t.Fatal("Expected result to have content")
|
||||
}
|
||||
|
||||
// Verify the content
|
||||
if textContent, ok := mcp.AsTextContent(result.Content[0]); ok {
|
||||
var resultTodos []TodoInfo
|
||||
if err := json.Unmarshal([]byte(textContent.Text), &resultTodos); err != nil {
|
||||
t.Errorf("Failed to parse result JSON: %v", err)
|
||||
} else if len(resultTodos) != 1 {
|
||||
t.Errorf("Expected 1 todo in result, got %d", len(resultTodos))
|
||||
} else if resultTodos[0].Content != "Existing task" {
|
||||
t.Errorf("Expected 'Existing task', got '%s'", resultTodos[0].Content)
|
||||
}
|
||||
} else {
|
||||
t.Error("Expected text content")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTodoValidation(t *testing.T) {
|
||||
server := &TodoServer{
|
||||
todos: make([]TodoInfo, 0),
|
||||
}
|
||||
|
||||
// Test invalid status
|
||||
request := mcp.CallToolRequest{
|
||||
Params: mcp.CallToolParams{
|
||||
Name: "todowrite",
|
||||
Arguments: map[string]any{
|
||||
"todos": []TodoInfo{
|
||||
{
|
||||
Content: "Test task",
|
||||
Status: "invalid_status",
|
||||
Priority: "high",
|
||||
ID: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := server.executeTodoWrite(ctx, request)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Should return an error result
|
||||
if result == nil {
|
||||
t.Fatal("Expected result to be non-nil")
|
||||
}
|
||||
|
||||
if len(result.Content) == 0 {
|
||||
t.Fatal("Expected result to have content")
|
||||
}
|
||||
|
||||
if textContent, ok := mcp.AsTextContent(result.Content[0]); ok {
|
||||
if textContent.Text == "" {
|
||||
t.Error("Expected non-empty error message")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTodoEmptyContent(t *testing.T) {
|
||||
server := &TodoServer{
|
||||
todos: make([]TodoInfo, 0),
|
||||
}
|
||||
|
||||
// Test empty content
|
||||
request := mcp.CallToolRequest{
|
||||
Params: mcp.CallToolParams{
|
||||
Name: "todowrite",
|
||||
Arguments: map[string]any{
|
||||
"todos": []TodoInfo{
|
||||
{
|
||||
Content: "",
|
||||
Status: "pending",
|
||||
Priority: "high",
|
||||
ID: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := server.executeTodoWrite(ctx, request)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Should return an error result
|
||||
if result == nil {
|
||||
t.Fatal("Expected result to be non-nil")
|
||||
}
|
||||
|
||||
if len(result.Content) == 0 {
|
||||
t.Fatal("Expected result to have content")
|
||||
}
|
||||
|
||||
if textContent, ok := mcp.AsTextContent(result.Content[0]); ok {
|
||||
if textContent.Text == "" {
|
||||
t.Error("Expected non-empty error message")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTodoActiveCounting(t *testing.T) {
|
||||
server := &TodoServer{
|
||||
todos: make([]TodoInfo, 0),
|
||||
}
|
||||
|
||||
// Create todos with different statuses
|
||||
todos := []TodoInfo{
|
||||
{Content: "Task 1", Status: "pending", Priority: "high", ID: "1"},
|
||||
{Content: "Task 2", Status: "in_progress", Priority: "medium", ID: "2"},
|
||||
{Content: "Task 3", Status: "completed", Priority: "low", ID: "3"},
|
||||
{Content: "Task 4", Status: "pending", Priority: "high", ID: "4"},
|
||||
}
|
||||
|
||||
request := mcp.CallToolRequest{
|
||||
Params: mcp.CallToolParams{
|
||||
Name: "todowrite",
|
||||
Arguments: map[string]any{
|
||||
"todos": todos,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := server.executeTodoWrite(ctx, request)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to execute todowrite: %v", err)
|
||||
}
|
||||
|
||||
// Check metadata for correct active count (should be 3: 2 pending + 1 in_progress)
|
||||
if result.Meta == nil {
|
||||
t.Fatal("Expected metadata to be non-nil")
|
||||
}
|
||||
|
||||
title, ok := result.Meta["title"].(string)
|
||||
if !ok {
|
||||
t.Fatal("Expected title in metadata")
|
||||
}
|
||||
|
||||
if title != "3 todos" {
|
||||
t.Errorf("Expected '3 todos', got '%s'", title)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user