first commit
This commit is contained in:
17
examples/mcps/auth-demo-server/go.mod
Normal file
17
examples/mcps/auth-demo-server/go.mod
Normal file
@@ -0,0 +1,17 @@
|
||||
module auth-demo-server
|
||||
|
||||
go 1.26.2
|
||||
|
||||
require github.com/mark3labs/mcp-go v0.43.2
|
||||
|
||||
require (
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
39
examples/mcps/auth-demo-server/go.sum
Normal file
39
examples/mcps/auth-demo-server/go.sum
Normal file
@@ -0,0 +1,39 @@
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
||||
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I=
|
||||
github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
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=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
227
examples/mcps/auth-demo-server/main.go
Normal file
227
examples/mcps/auth-demo-server/main.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package main
|
||||
|
||||
// auth-demo-server demonstrates two layers of authentication for HTTP MCP servers:
|
||||
//
|
||||
// 1. CONNECTION-LEVEL AUTH (X-API-Key header)
|
||||
// Enforced in HTTP middleware on every request (initialize, tools/list,
|
||||
// tools/call). A missing or wrong key is rejected before the MCP server
|
||||
// sees the message at all.
|
||||
//
|
||||
// 2. TOOL-EXECUTION AUTH (X-Tool-Token header)
|
||||
// A separate secret token checked exclusively inside sensitive tool handlers
|
||||
// at call time. Public tools ignore it; the connection middleware does not
|
||||
// inspect it at all. This lets you scope a second credential to tool
|
||||
// execution only — distinct from the connection credential.
|
||||
//
|
||||
// HOW BIFROST SENDS HEADERS
|
||||
//
|
||||
// Bifrost has a single `headers` field on MCPClientConfig. Those same headers are
|
||||
// used in two places:
|
||||
// - At connection time: passed to transport.WithHTTPHeaders() so every HTTP
|
||||
// request to the server carries them.
|
||||
// - At tool-call time: copied onto CallToolRequest.Header so the server can
|
||||
// read them inside the tool handler via the request context.
|
||||
//
|
||||
// This means all configured headers are present on EVERY request — there is no
|
||||
// separate "connection-only" vs "tool-only" header mechanism in Bifrost. To
|
||||
// distinguish the two auth levels you simply use different header names, both
|
||||
// configured in the same `headers` map. The server then enforces each header
|
||||
// at the appropriate layer (middleware vs. handler).
|
||||
//
|
||||
// Bifrost config example:
|
||||
//
|
||||
// {
|
||||
// "name": "auth_demo",
|
||||
// "connection_type": "http",
|
||||
// "connection_string": "http://localhost:3002/",
|
||||
// "auth_type": "headers",
|
||||
// "headers": {
|
||||
// "X-API-Key": "super-secret-key",
|
||||
// "X-Tool-Token": "tool-exec-secret"
|
||||
// },
|
||||
// "tools_to_execute": ["*"]
|
||||
// }
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
const (
|
||||
// connectionAPIKey is checked in HTTP middleware on every request
|
||||
// (initialize, tools/list, tools/call).
|
||||
// In production, load this from an environment variable or secrets manager.
|
||||
connectionAPIKey = "super-secret-key"
|
||||
|
||||
// toolExecToken is checked exclusively inside sensitive tool handlers —
|
||||
// never in the connection middleware. It acts as a second independent
|
||||
// credential that gates tool execution only.
|
||||
// In production, load this from an environment variable or secrets manager.
|
||||
toolExecToken = "tool-exec-secret"
|
||||
)
|
||||
|
||||
// contextKey is a private type so we don't collide with other packages' context keys.
|
||||
type contextKey string
|
||||
|
||||
const requestHeadersKey contextKey = "request_headers"
|
||||
|
||||
func main() {
|
||||
s := server.NewMCPServer("auth-demo-server", "1.0.0")
|
||||
|
||||
// public_info only requires connection-level auth (X-API-Key).
|
||||
// Any authenticated client can call it without a tool execution token.
|
||||
publicTool := mcp.NewTool(
|
||||
"public_info",
|
||||
mcp.WithDescription("Returns non-sensitive public information. Requires connection auth (X-API-Key) only."),
|
||||
mcp.WithString("topic", mcp.Required(), mcp.Description("Topic to look up")),
|
||||
)
|
||||
s.AddTool(publicTool, publicInfoHandler)
|
||||
|
||||
// secret_data requires BOTH connection-level auth (X-API-Key) AND a
|
||||
// dedicated tool-execution token (X-Tool-Token) checked inside the handler.
|
||||
// In Bifrost both headers live in the same `headers` map and arrive on
|
||||
// every request, so the handler reads X-Tool-Token from context and
|
||||
// validates it independently of the connection credential.
|
||||
secretTool := mcp.NewTool(
|
||||
"secret_data",
|
||||
mcp.WithDescription("Returns sensitive data. Requires connection auth (X-API-Key) AND tool-execution auth (X-Tool-Token)."),
|
||||
mcp.WithString("resource", mcp.Required(), mcp.Description("Resource name to fetch")),
|
||||
)
|
||||
s.AddTool(secretTool, secretDataHandler)
|
||||
|
||||
httpServer := server.NewStreamableHTTPServer(s)
|
||||
|
||||
// Middleware chain (outermost = first to run):
|
||||
// 1. connectionAuthMiddleware — rejects requests with a wrong/missing X-API-Key
|
||||
// 2. injectHeadersMiddleware — stores the request headers in context so
|
||||
// tool handlers can read them for tool-level auth
|
||||
// 3. httpServer — the MCP server itself
|
||||
handler := connectionAuthMiddleware(injectHeadersMiddleware(httpServer))
|
||||
|
||||
addr := "localhost:3002"
|
||||
log.Printf("auth-demo-server listening on http://%s/", addr)
|
||||
log.Printf("\nAuth layers:")
|
||||
log.Printf(" Connection-level: X-API-Key: %s (middleware rejects all requests without it)", connectionAPIKey)
|
||||
log.Printf(" Tool-execution: X-Tool-Token: %s (only secret_data checks this, validated inside the handler)", toolExecToken)
|
||||
log.Printf("\nNote: Bifrost sends all `headers` on both connection setup AND every tool call.")
|
||||
log.Printf("Both X-API-Key and X-Tool-Token go in the same `headers` map.")
|
||||
log.Printf("The server enforces each at the right layer: middleware vs. handler.\n")
|
||||
log.Printf("Bifrost config:")
|
||||
log.Printf(`
|
||||
{
|
||||
"name": "auth_demo",
|
||||
"connection_type": "http",
|
||||
"connection_string": "http://%s/",
|
||||
"auth_type": "headers",
|
||||
"headers": {
|
||||
"X-API-Key": "%s",
|
||||
"X-Tool-Token": "%s"
|
||||
},
|
||||
"tools_to_execute": ["*"]
|
||||
}
|
||||
`, addr, connectionAPIKey, toolExecToken)
|
||||
|
||||
if err := http.ListenAndServe(addr, handler); err != nil {
|
||||
log.Fatalf("Server error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Middleware ───────────────────────────────────────────────────────────────
|
||||
|
||||
// connectionAuthMiddleware enforces connection-level authentication.
|
||||
// Every HTTP request — including initialize, tools/list, and tools/call —
|
||||
// must carry the correct X-API-Key header. A missing or wrong key results
|
||||
// in HTTP 401 before the MCP server processes anything.
|
||||
func connectionAuthMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
key := r.Header.Get("X-API-Key")
|
||||
if key == "" {
|
||||
http.Error(w, "connection auth required: missing X-API-Key header", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
if key != connectionAPIKey {
|
||||
http.Error(w, "connection auth failed: invalid X-API-Key", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// injectHeadersMiddleware stores the raw HTTP request headers in the context
|
||||
// so that tool handlers can read them for tool-level auth checks.
|
||||
// This is needed because MCP tool handlers only receive (ctx, CallToolRequest)
|
||||
// — they don't have direct access to the http.Request.
|
||||
func injectHeadersMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.WithValue(r.Context(), requestHeadersKey, r.Header)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Tool handlers ────────────────────────────────────────────────────────────
|
||||
|
||||
// publicInfoHandler handles "public_info". Connection auth has already been
|
||||
// verified by middleware, so no further auth check is needed here.
|
||||
func publicInfoHandler(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
var args struct {
|
||||
Topic string `json:"topic"`
|
||||
}
|
||||
if err := parseArgs(req, &args); err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(fmt.Sprintf(
|
||||
"Public info about %q: this data is available to all authenticated clients.", args.Topic,
|
||||
)), nil
|
||||
}
|
||||
|
||||
// secretDataHandler handles "secret_data". Connection-level auth (X-API-Key)
|
||||
// has already been verified by middleware. Here we additionally check
|
||||
// X-Tool-Token — a separate secret dedicated to authorizing tool execution.
|
||||
// Bifrost sends it as part of the same `headers` map, so it arrives on every
|
||||
// request including this tool call; the middleware intentionally ignores it.
|
||||
func secretDataHandler(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
// ── Tool-execution token check ───────────────────────────────────────────
|
||||
headers, ok := ctx.Value(requestHeadersKey).(http.Header)
|
||||
if !ok {
|
||||
return mcp.NewToolResultError("tool auth error: request headers unavailable in context"), nil
|
||||
}
|
||||
token := headers.Get("X-Tool-Token")
|
||||
if token == "" {
|
||||
return mcp.NewToolResultError("tool auth required: missing X-Tool-Token header"), nil
|
||||
}
|
||||
if token != toolExecToken {
|
||||
return mcp.NewToolResultError("tool auth failed: invalid X-Tool-Token"), nil
|
||||
}
|
||||
// ── Auth passed, proceed ─────────────────────────────────────────────────
|
||||
|
||||
var args struct {
|
||||
Resource string `json:"resource"`
|
||||
}
|
||||
if err := parseArgs(req, &args); err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(fmt.Sprintf(
|
||||
"Secret data for resource %q: [classified content — X-API-Key + X-Tool-Token verified]", args.Resource,
|
||||
)), nil
|
||||
}
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func parseArgs(req mcp.CallToolRequest, dst any) error {
|
||||
b, err := json.Marshal(req.Params.Arguments)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal arguments: %w", err)
|
||||
}
|
||||
if err := json.Unmarshal(b, dst); err != nil {
|
||||
return fmt.Errorf("invalid arguments: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
72
examples/mcps/edge-case-server/README.md
Normal file
72
examples/mcps/edge-case-server/README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Edge Case MCP Server
|
||||
|
||||
MCP STDIO server optimized for testing edge cases and unusual scenarios.
|
||||
|
||||
## Tools
|
||||
|
||||
- **unicode_tool** - Returns Unicode text including emojis and right-to-left characters
|
||||
- **binary_data** - Returns binary-like data in various encodings (base64, hex, raw)
|
||||
- **empty_response** - Returns various types of empty responses (empty string, object, array, null)
|
||||
- **null_fields** - Returns responses with configurable null fields
|
||||
- **deeply_nested** - Returns deeply nested data structures up to specified depth
|
||||
- **special_chars** - Returns text with special characters (quotes, backslashes, newlines, control chars)
|
||||
- **zero_length** - Returns zero-length content
|
||||
- **extreme_sizes** - Returns data of various extreme sizes (tiny, normal, huge)
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build
|
||||
npm run build
|
||||
|
||||
# Run
|
||||
node dist/index.js
|
||||
```
|
||||
|
||||
## Integration Testing
|
||||
|
||||
This server is designed to test edge case handling in Bifrost's MCP integration via STDIO transport.
|
||||
|
||||
### Example Tool Calls
|
||||
|
||||
```typescript
|
||||
// Test Unicode handling
|
||||
{
|
||||
"name": "unicode_tool",
|
||||
"arguments": {
|
||||
"id": "test-1",
|
||||
"include_emojis": true,
|
||||
"include_rtl": true
|
||||
}
|
||||
}
|
||||
|
||||
// Test binary data
|
||||
{
|
||||
"name": "binary_data",
|
||||
"arguments": {
|
||||
"id": "test-2",
|
||||
"encoding": "base64"
|
||||
}
|
||||
}
|
||||
|
||||
// Test deeply nested structures
|
||||
{
|
||||
"name": "deeply_nested",
|
||||
"arguments": {
|
||||
"id": "test-3",
|
||||
"depth": 20
|
||||
}
|
||||
}
|
||||
|
||||
// Test special characters
|
||||
{
|
||||
"name": "special_chars",
|
||||
"arguments": {
|
||||
"id": "test-4",
|
||||
"char_type": "all"
|
||||
}
|
||||
}
|
||||
```
|
||||
17
examples/mcps/edge-case-server/go.mod
Normal file
17
examples/mcps/edge-case-server/go.mod
Normal file
@@ -0,0 +1,17 @@
|
||||
module github.com/maximhq/bifrost/examples/mcps/edge-case-server
|
||||
|
||||
go 1.26.2
|
||||
|
||||
require github.com/mark3labs/mcp-go v0.43.2
|
||||
|
||||
require (
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
39
examples/mcps/edge-case-server/go.sum
Normal file
39
examples/mcps/edge-case-server/go.sum
Normal file
@@ -0,0 +1,39 @@
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
||||
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I=
|
||||
github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
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=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
325
examples/mcps/edge-case-server/main.go
Normal file
325
examples/mcps/edge-case-server/main.go
Normal file
@@ -0,0 +1,325 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create MCP server
|
||||
s := server.NewMCPServer(
|
||||
"edge-case-server",
|
||||
"1.0.0",
|
||||
server.WithToolCapabilities(true),
|
||||
)
|
||||
|
||||
// Register all tools
|
||||
registerReturnUnicodeTool(s)
|
||||
registerReturnBinaryTool(s)
|
||||
registerReturnLargePayloadTool(s)
|
||||
registerReturnNestedStructureTool(s)
|
||||
registerReturnNullTool(s)
|
||||
registerReturnSpecialCharsTool(s)
|
||||
|
||||
// Start STDIO server
|
||||
if err := server.ServeStdio(s); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Server error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 1: return_unicode
|
||||
// ============================================================================
|
||||
|
||||
func registerReturnUnicodeTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("return_unicode",
|
||||
mcp.WithDescription("Returns unicode strings of various types"),
|
||||
mcp.WithString("type",
|
||||
mcp.Required(),
|
||||
mcp.Description("Type of unicode to return"),
|
||||
mcp.Enum("emoji"),
|
||||
),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
var args struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// Get arguments using the proper method
|
||||
argsInterface := request.GetArguments()
|
||||
|
||||
// Marshal and unmarshal to convert to our struct
|
||||
argsBytes, err := json.Marshal(argsInterface)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(argsBytes, &args); err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Invalid arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
var text string
|
||||
switch args.Type {
|
||||
case "emoji":
|
||||
text = "Hello 👋 World 🌍! Testing emoji: 🎉 🚀 💻 ❤️ 🔥"
|
||||
default:
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Unknown type: %s", args.Type)), nil
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"type": args.Type,
|
||||
"text": text,
|
||||
"length": len([]rune(text)),
|
||||
}
|
||||
|
||||
jsonResult, _ := json.Marshal(response)
|
||||
return mcp.NewToolResultText(string(jsonResult)), nil
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 2: return_binary
|
||||
// ============================================================================
|
||||
|
||||
func registerReturnBinaryTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("return_binary",
|
||||
mcp.WithDescription("Returns binary data in specified encoding"),
|
||||
mcp.WithNumber("size",
|
||||
mcp.Required(),
|
||||
mcp.Description("Size of binary data in bytes"),
|
||||
),
|
||||
mcp.WithString("encoding",
|
||||
mcp.Required(),
|
||||
mcp.Description("Encoding for binary data"),
|
||||
mcp.Enum("base64", "hex"),
|
||||
),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
var args struct {
|
||||
Size int `json:"size"`
|
||||
Encoding string `json:"encoding"`
|
||||
}
|
||||
|
||||
// Get arguments using the proper method
|
||||
argsInterface := request.GetArguments()
|
||||
|
||||
// Marshal and unmarshal to convert to our struct
|
||||
argsBytes, err := json.Marshal(argsInterface)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(argsBytes, &args); err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Invalid arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
// Generate binary data (repeating pattern)
|
||||
data := make([]byte, args.Size)
|
||||
for i := range data {
|
||||
data[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
var encoded string
|
||||
switch args.Encoding {
|
||||
case "base64":
|
||||
encoded = base64.StdEncoding.EncodeToString(data)
|
||||
case "hex":
|
||||
encoded = hex.EncodeToString(data)
|
||||
default:
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Unknown encoding: %s", args.Encoding)), nil
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"size": args.Size,
|
||||
"encoding": args.Encoding,
|
||||
"data": encoded,
|
||||
}
|
||||
|
||||
jsonResult, _ := json.Marshal(response)
|
||||
return mcp.NewToolResultText(string(jsonResult)), nil
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 3: return_large_payload
|
||||
// ============================================================================
|
||||
|
||||
func registerReturnLargePayloadTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("return_large_payload",
|
||||
mcp.WithDescription("Returns a large JSON payload"),
|
||||
mcp.WithNumber("size_kb",
|
||||
mcp.Required(),
|
||||
mcp.Description("Approximate size in kilobytes"),
|
||||
),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
var args struct {
|
||||
SizeKB int `json:"size_kb"`
|
||||
}
|
||||
|
||||
// Get arguments using the proper method
|
||||
argsInterface := request.GetArguments()
|
||||
|
||||
// Marshal and unmarshal to convert to our struct
|
||||
argsBytes, err := json.Marshal(argsInterface)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(argsBytes, &args); err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Invalid arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
// Generate array of objects to reach target size
|
||||
targetSize := args.SizeKB * 1024
|
||||
items := []map[string]interface{}{}
|
||||
currentSize := 0
|
||||
|
||||
for currentSize < targetSize {
|
||||
item := map[string]interface{}{
|
||||
"id": len(items),
|
||||
"name": fmt.Sprintf("Item-%d", len(items)),
|
||||
"description": "This is a test item with some text to increase the payload size.",
|
||||
"value": len(items) * 100,
|
||||
"active": len(items)%2 == 0,
|
||||
"tags": []string{"tag1", "tag2", "tag3"},
|
||||
}
|
||||
items = append(items, item)
|
||||
|
||||
// Rough estimate of current size
|
||||
itemJSON, _ := json.Marshal(item)
|
||||
currentSize += len(itemJSON)
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"requested_size_kb": args.SizeKB,
|
||||
"item_count": len(items),
|
||||
"items": items,
|
||||
}
|
||||
|
||||
jsonResult, _ := json.Marshal(response)
|
||||
return mcp.NewToolResultText(string(jsonResult)), nil
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 4: return_nested_structure
|
||||
// ============================================================================
|
||||
|
||||
func registerReturnNestedStructureTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("return_nested_structure",
|
||||
mcp.WithDescription("Returns deeply nested JSON structure"),
|
||||
mcp.WithNumber("depth",
|
||||
mcp.Required(),
|
||||
mcp.Description("Depth of nesting"),
|
||||
),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
var args struct {
|
||||
Depth int `json:"depth"`
|
||||
}
|
||||
|
||||
// Get arguments using the proper method
|
||||
argsInterface := request.GetArguments()
|
||||
|
||||
// Marshal and unmarshal to convert to our struct
|
||||
argsBytes, err := json.Marshal(argsInterface)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(argsBytes, &args); err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Invalid arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
// Build nested structure
|
||||
nested := buildNestedStructure(args.Depth)
|
||||
|
||||
response := map[string]interface{}{
|
||||
"depth": args.Depth,
|
||||
"data": nested,
|
||||
}
|
||||
|
||||
jsonResult, _ := json.Marshal(response)
|
||||
return mcp.NewToolResultText(string(jsonResult)), nil
|
||||
})
|
||||
}
|
||||
|
||||
func buildNestedStructure(depth int) map[string]interface{} {
|
||||
if depth <= 0 {
|
||||
return map[string]interface{}{
|
||||
"level": 0,
|
||||
"value": "leaf node",
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"level": depth,
|
||||
"child": buildNestedStructure(depth - 1),
|
||||
"data": map[string]interface{}{
|
||||
"id": depth,
|
||||
"name": fmt.Sprintf("Level %d", depth),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 5: return_null
|
||||
// ============================================================================
|
||||
|
||||
func registerReturnNullTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("return_null",
|
||||
mcp.WithDescription("Returns null/empty values in various forms"),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
response := map[string]interface{}{
|
||||
"null_value": nil,
|
||||
"empty_string": "",
|
||||
"empty_array": []interface{}{},
|
||||
"empty_object": map[string]interface{}{},
|
||||
"zero": 0,
|
||||
"false": false,
|
||||
}
|
||||
|
||||
jsonResult, _ := json.Marshal(response)
|
||||
return mcp.NewToolResultText(string(jsonResult)), nil
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 6: return_special_chars
|
||||
// ============================================================================
|
||||
|
||||
func registerReturnSpecialCharsTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("return_special_chars",
|
||||
mcp.WithDescription("Returns strings with special characters and escape sequences"),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
response := map[string]interface{}{
|
||||
"quotes": `He said "Hello" and she said 'Hi'`,
|
||||
"backslashes": `C:\Users\Test\Path`,
|
||||
"newlines": "Line 1\nLine 2\nLine 3",
|
||||
"tabs": "Col1\tCol2\tCol3",
|
||||
"mixed": "Special: \t\n\r\\ \" ' / @ # $ % & * ( )",
|
||||
"unicode_escape": "\u0041\u0042\u0043", // ABC
|
||||
"control_chars": "\x00\x01\x02",
|
||||
}
|
||||
|
||||
jsonResult, _ := json.Marshal(response)
|
||||
return mcp.NewToolResultText(string(jsonResult)), nil
|
||||
})
|
||||
}
|
||||
1161
examples/mcps/edge-case-server/package-lock.json
generated
Normal file
1161
examples/mcps/edge-case-server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
examples/mcps/edge-case-server/package.json
Normal file
24
examples/mcps/edge-case-server/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "edge-case-server",
|
||||
"version": "1.0.0",
|
||||
"description": "MCP STDIO server optimized for testing edge cases and unusual scenarios",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"edge-case-server": "./dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc && chmod +x dist/index.js",
|
||||
"prepare": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"overrides": {
|
||||
"hono": "4.12.14"
|
||||
}
|
||||
}
|
||||
456
examples/mcps/edge-case-server/src/index.ts
Normal file
456
examples/mcps/edge-case-server/src/index.ts
Normal file
@@ -0,0 +1,456 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { z } from "zod";
|
||||
|
||||
// Schemas for edge case test tools
|
||||
const UnicodeToolSchema = z.object({
|
||||
id: z.string().describe("Tool invocation ID"),
|
||||
include_emojis: z.boolean().optional().describe("Include emoji characters"),
|
||||
include_rtl: z.boolean().optional().describe("Include right-to-left text"),
|
||||
});
|
||||
|
||||
const BinaryDataSchema = z.object({
|
||||
id: z.string().describe("Tool invocation ID"),
|
||||
encoding: z.enum(["base64", "hex", "raw"]).optional(),
|
||||
});
|
||||
|
||||
const EmptyResponseSchema = z.object({
|
||||
id: z.string().describe("Tool invocation ID"),
|
||||
type: z.enum(["empty_string", "empty_object", "empty_array", "null"]).optional(),
|
||||
});
|
||||
|
||||
const NullFieldsSchema = z.object({
|
||||
id: z.string().describe("Tool invocation ID"),
|
||||
null_count: z.number().optional().describe("Number of null fields to include"),
|
||||
});
|
||||
|
||||
const DeeplyNestedSchema = z.object({
|
||||
id: z.string().describe("Tool invocation ID"),
|
||||
depth: z.number().optional().describe("Nesting depth (default 10)"),
|
||||
});
|
||||
|
||||
const SpecialCharsSchema = z.object({
|
||||
id: z.string().describe("Tool invocation ID"),
|
||||
char_type: z.enum(["quotes", "backslashes", "newlines", "control_chars", "all"]).optional(),
|
||||
});
|
||||
|
||||
const ZeroLengthSchema = z.object({
|
||||
id: z.string().describe("Tool invocation ID"),
|
||||
});
|
||||
|
||||
const ExtremeSizesSchema = z.object({
|
||||
id: z.string().describe("Tool invocation ID"),
|
||||
size_type: z.enum(["tiny", "normal", "huge"]).optional(),
|
||||
});
|
||||
|
||||
const server = new Server(
|
||||
{ name: "edge-case-server", version: "1.0.0" },
|
||||
{ capabilities: { tools: {} } }
|
||||
);
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [
|
||||
{
|
||||
name: "unicode_tool",
|
||||
description: "Returns Unicode text including emojis and RTL characters",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
include_emojis: {
|
||||
type: "boolean",
|
||||
description: "Include emoji characters",
|
||||
},
|
||||
include_rtl: {
|
||||
type: "boolean",
|
||||
description: "Include right-to-left text",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "binary_data",
|
||||
description: "Returns binary-like data in various encodings",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
encoding: {
|
||||
type: "string",
|
||||
enum: ["base64", "hex", "raw"],
|
||||
description: "Data encoding format",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty_response",
|
||||
description: "Returns various types of empty responses",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
type: {
|
||||
type: "string",
|
||||
enum: ["empty_string", "empty_object", "empty_array", "null"],
|
||||
description: "Type of empty response",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "null_fields",
|
||||
description: "Returns responses with null fields",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
null_count: {
|
||||
type: "number",
|
||||
description: "Number of null fields to include",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "deeply_nested",
|
||||
description: "Returns deeply nested data structures",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
depth: {
|
||||
type: "number",
|
||||
description: "Nesting depth (default 10)",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "special_chars",
|
||||
description: "Returns text with special characters that need escaping",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
char_type: {
|
||||
type: "string",
|
||||
enum: ["quotes", "backslashes", "newlines", "control_chars", "all"],
|
||||
description: "Type of special characters to include",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "zero_length",
|
||||
description: "Returns zero-length content",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "extreme_sizes",
|
||||
description: "Returns data of various extreme sizes",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
size_type: {
|
||||
type: "string",
|
||||
enum: ["tiny", "normal", "huge"],
|
||||
description: "Size category",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const toolName = request.params.name;
|
||||
|
||||
try {
|
||||
switch (toolName) {
|
||||
case "unicode_tool": {
|
||||
const args = UnicodeToolSchema.parse(request.params.arguments);
|
||||
let text = "Unicode test: ";
|
||||
|
||||
// Basic Unicode characters
|
||||
text += "Ω α β γ δ ε ζ η θ ";
|
||||
|
||||
if (args.include_emojis) {
|
||||
text += "😀 😎 🔧 🚀 🎉 🌟 💻 🐍 ";
|
||||
}
|
||||
|
||||
if (args.include_rtl) {
|
||||
text += "مرحبا 你好 שלום ";
|
||||
}
|
||||
|
||||
// Additional Unicode ranges
|
||||
text += "© ® ™ € £ ¥ ";
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
tool: "unicode_tool",
|
||||
id: args.id,
|
||||
unicode_text: text,
|
||||
include_emojis: args.include_emojis ?? false,
|
||||
include_rtl: args.include_rtl ?? false,
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "binary_data": {
|
||||
const args = BinaryDataSchema.parse(request.params.arguments);
|
||||
const encoding = args.encoding || "base64";
|
||||
|
||||
const binaryData = Buffer.from("This is binary data \x00\x01\x02\x03\xff\xfe");
|
||||
let encodedData: string;
|
||||
|
||||
switch (encoding) {
|
||||
case "base64":
|
||||
encodedData = binaryData.toString("base64");
|
||||
break;
|
||||
case "hex":
|
||||
encodedData = binaryData.toString("hex");
|
||||
break;
|
||||
case "raw":
|
||||
encodedData = binaryData.toString("binary");
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
tool: "binary_data",
|
||||
id: args.id,
|
||||
encoding,
|
||||
data: encodedData,
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "empty_response": {
|
||||
const args = EmptyResponseSchema.parse(request.params.arguments);
|
||||
const type = args.type || "empty_string";
|
||||
|
||||
let responseData: any;
|
||||
switch (type) {
|
||||
case "empty_string":
|
||||
responseData = "";
|
||||
break;
|
||||
case "empty_object":
|
||||
responseData = {};
|
||||
break;
|
||||
case "empty_array":
|
||||
responseData = [];
|
||||
break;
|
||||
case "null":
|
||||
responseData = null;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
tool: "empty_response",
|
||||
id: args.id,
|
||||
type,
|
||||
data: responseData,
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "null_fields": {
|
||||
const args = NullFieldsSchema.parse(request.params.arguments);
|
||||
const nullCount = args.null_count || 3;
|
||||
|
||||
const response: any = {
|
||||
tool: "null_fields",
|
||||
id: args.id,
|
||||
};
|
||||
|
||||
// Add null fields
|
||||
for (let i = 0; i < nullCount; i++) {
|
||||
response[`null_field_${i + 1}`] = null;
|
||||
}
|
||||
|
||||
response.non_null_field = "This is not null";
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(response),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "deeply_nested": {
|
||||
const args = DeeplyNestedSchema.parse(request.params.arguments);
|
||||
const depth = args.depth || 10;
|
||||
|
||||
// Create deeply nested structure
|
||||
let nested: any = { value: "leaf" };
|
||||
for (let i = 0; i < depth; i++) {
|
||||
nested = {
|
||||
level: depth - i,
|
||||
child: nested,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
tool: "deeply_nested",
|
||||
id: args.id,
|
||||
depth,
|
||||
data: nested,
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "special_chars": {
|
||||
const args = SpecialCharsSchema.parse(request.params.arguments);
|
||||
const charType = args.char_type || "all";
|
||||
|
||||
let text = "";
|
||||
|
||||
if (charType === "quotes" || charType === "all") {
|
||||
text += 'Text with "double quotes" and \'single quotes\' ';
|
||||
}
|
||||
|
||||
if (charType === "backslashes" || charType === "all") {
|
||||
text += "Path: C:\\Users\\Test\\file.txt ";
|
||||
}
|
||||
|
||||
if (charType === "newlines" || charType === "all") {
|
||||
text += "Line 1\nLine 2\r\nLine 3\tTabbed ";
|
||||
}
|
||||
|
||||
if (charType === "control_chars" || charType === "all") {
|
||||
text += "Control: \x00 \x01 \x1F ";
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
tool: "special_chars",
|
||||
id: args.id,
|
||||
char_type: charType,
|
||||
text,
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "zero_length": {
|
||||
const args = ZeroLengthSchema.parse(request.params.arguments);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "extreme_sizes": {
|
||||
const args = ExtremeSizesSchema.parse(request.params.arguments);
|
||||
const sizeType = args.size_type || "normal";
|
||||
|
||||
let data: string;
|
||||
switch (sizeType) {
|
||||
case "tiny":
|
||||
data = "x";
|
||||
break;
|
||||
case "normal":
|
||||
data = "x".repeat(1000);
|
||||
break;
|
||||
case "huge":
|
||||
data = "x".repeat(1000000); // 1MB
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
tool: "extreme_sizes",
|
||||
id: args.id,
|
||||
size_type: sizeType,
|
||||
data_length: data.length,
|
||||
data,
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("Edge Case MCP Server running on stdio");
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Fatal error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
17
examples/mcps/edge-case-server/tsconfig.json
Normal file
17
examples/mcps/edge-case-server/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
70
examples/mcps/error-test-server/README.md
Normal file
70
examples/mcps/error-test-server/README.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Error Test MCP Server
|
||||
|
||||
MCP STDIO server optimized for testing error scenarios and edge cases.
|
||||
|
||||
## Tools
|
||||
|
||||
- **malformed_json** - Returns malformed JSON (truncated, invalid escapes, unclosed brackets, mixed types)
|
||||
- **timeout_tool** - Hangs for specified duration to test timeout handling
|
||||
- **intermittent_fail** - Randomly fails based on fail_rate to test retry logic
|
||||
- **network_error** - Simulates network errors (connection refused, timeout, DNS failure, SSL errors)
|
||||
- **large_payload** - Returns very large payloads to test size limits
|
||||
- **partial_response** - Returns incomplete responses to test handling
|
||||
- **invalid_content_type** - Returns content with mismatched type declaration
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build
|
||||
npm run build
|
||||
|
||||
# Run
|
||||
node dist/index.js
|
||||
```
|
||||
|
||||
## Integration Testing
|
||||
|
||||
This server is designed to test error handling in Bifrost's MCP integration via STDIO transport.
|
||||
|
||||
### Example Tool Calls
|
||||
|
||||
```typescript
|
||||
// Test malformed JSON
|
||||
{
|
||||
"name": "malformed_json",
|
||||
"arguments": {
|
||||
"id": "test-1",
|
||||
"json_type": "truncated"
|
||||
}
|
||||
}
|
||||
|
||||
// Test timeout
|
||||
{
|
||||
"name": "timeout_tool",
|
||||
"arguments": {
|
||||
"id": "test-2",
|
||||
"timeout_ms": 3000
|
||||
}
|
||||
}
|
||||
|
||||
// Test intermittent failures
|
||||
{
|
||||
"name": "intermittent_fail",
|
||||
"arguments": {
|
||||
"id": "test-3",
|
||||
"fail_rate": 0.7
|
||||
}
|
||||
}
|
||||
|
||||
// Test large payloads
|
||||
{
|
||||
"name": "large_payload",
|
||||
"arguments": {
|
||||
"id": "test-4",
|
||||
"size_kb": 500
|
||||
}
|
||||
}
|
||||
```
|
||||
17
examples/mcps/error-test-server/go.mod
Normal file
17
examples/mcps/error-test-server/go.mod
Normal file
@@ -0,0 +1,17 @@
|
||||
module github.com/maximhq/bifrost/examples/mcps/error-test-server
|
||||
|
||||
go 1.26.2
|
||||
|
||||
require github.com/mark3labs/mcp-go v0.43.2
|
||||
|
||||
require (
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
39
examples/mcps/error-test-server/go.sum
Normal file
39
examples/mcps/error-test-server/go.sum
Normal file
@@ -0,0 +1,39 @@
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
||||
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I=
|
||||
github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
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=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
279
examples/mcps/error-test-server/main.go
Normal file
279
examples/mcps/error-test-server/main.go
Normal file
@@ -0,0 +1,279 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Seed random number generator
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
// Create MCP server
|
||||
s := server.NewMCPServer(
|
||||
"error-test-server",
|
||||
"1.0.0",
|
||||
server.WithToolCapabilities(true),
|
||||
)
|
||||
|
||||
// Register all tools
|
||||
registerTimeoutAfterTool(s)
|
||||
registerReturnMalformedJSONTool(s)
|
||||
registerReturnErrorTool(s)
|
||||
registerIntermittentFailTool(s)
|
||||
registerMemoryIntensiveTool(s)
|
||||
|
||||
// Start STDIO server
|
||||
if err := server.ServeStdio(s); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Server error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 1: timeout_after
|
||||
// ============================================================================
|
||||
|
||||
func registerTimeoutAfterTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("timeout_after",
|
||||
mcp.WithDescription("Simulates a timeout by delaying for specified seconds"),
|
||||
mcp.WithNumber("seconds",
|
||||
mcp.Required(),
|
||||
mcp.Description("Number of seconds to wait before responding"),
|
||||
),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
var args struct {
|
||||
Seconds float64 `json:"seconds"`
|
||||
}
|
||||
|
||||
// Get arguments using the proper method
|
||||
argsInterface := request.GetArguments()
|
||||
|
||||
// Marshal and unmarshal to convert to our struct
|
||||
argsBytes, err := json.Marshal(argsInterface)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(argsBytes, &args); err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Invalid arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
duration := time.Duration(args.Seconds * float64(time.Second))
|
||||
|
||||
// Use context-aware sleep
|
||||
select {
|
||||
case <-time.After(duration):
|
||||
response := map[string]interface{}{
|
||||
"delayed_seconds": args.Seconds,
|
||||
"message": fmt.Sprintf("Delayed for %.2f seconds", args.Seconds),
|
||||
}
|
||||
jsonResult, _ := json.Marshal(response)
|
||||
return mcp.NewToolResultText(string(jsonResult)), nil
|
||||
case <-ctx.Done():
|
||||
return mcp.NewToolResultError("Operation cancelled or timed out"), nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 2: return_malformed_json
|
||||
// ============================================================================
|
||||
|
||||
func registerReturnMalformedJSONTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("return_malformed_json",
|
||||
mcp.WithDescription("Returns intentionally malformed JSON to test error handling"),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
// Return deliberately broken JSON
|
||||
// Note: This will be wrapped in the MCP protocol, so the MCP layer should handle it
|
||||
// But the content itself is invalid JSON
|
||||
malformedJSON := `{"key": "value", "broken": }`
|
||||
return mcp.NewToolResultText(malformedJSON), nil
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 3: return_error
|
||||
// ============================================================================
|
||||
|
||||
func registerReturnErrorTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("return_error",
|
||||
mcp.WithDescription("Returns an error with specified type"),
|
||||
mcp.WithString("error_type",
|
||||
mcp.Required(),
|
||||
mcp.Description("Type of error to return"),
|
||||
mcp.Enum("validation", "runtime", "network", "timeout", "permission"),
|
||||
),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
var args struct {
|
||||
ErrorType string `json:"error_type"`
|
||||
}
|
||||
|
||||
// Get arguments using the proper method
|
||||
argsInterface := request.GetArguments()
|
||||
|
||||
// Marshal and unmarshal to convert to our struct
|
||||
argsBytes, err := json.Marshal(argsInterface)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(argsBytes, &args); err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Invalid arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
var errorMessage string
|
||||
switch args.ErrorType {
|
||||
case "validation":
|
||||
errorMessage = "Validation Error: Invalid input parameters provided"
|
||||
case "runtime":
|
||||
errorMessage = "Runtime Error: Unexpected condition occurred during execution"
|
||||
case "network":
|
||||
errorMessage = "Network Error: Failed to connect to remote service"
|
||||
case "timeout":
|
||||
errorMessage = "Timeout Error: Operation exceeded maximum allowed time"
|
||||
case "permission":
|
||||
errorMessage = "Permission Error: Insufficient privileges to perform operation"
|
||||
default:
|
||||
errorMessage = fmt.Sprintf("Unknown error type: %s", args.ErrorType)
|
||||
}
|
||||
|
||||
return mcp.NewToolResultError(errorMessage), nil
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 4: intermittent_fail
|
||||
// ============================================================================
|
||||
|
||||
func registerIntermittentFailTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("intermittent_fail",
|
||||
mcp.WithDescription("Fails randomly based on specified fail rate percentage (0-100)"),
|
||||
mcp.WithNumber("fail_rate",
|
||||
mcp.Required(),
|
||||
mcp.Description("Percentage chance of failure (0-100)"),
|
||||
),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
var args struct {
|
||||
FailRate float64 `json:"fail_rate"`
|
||||
}
|
||||
|
||||
// Get arguments using the proper method
|
||||
argsInterface := request.GetArguments()
|
||||
|
||||
// Marshal and unmarshal to convert to our struct
|
||||
argsBytes, err := json.Marshal(argsInterface)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(argsBytes, &args); err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Invalid arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
// Validate fail rate
|
||||
if args.FailRate < 0 || args.FailRate > 100 {
|
||||
return mcp.NewToolResultError("Fail rate must be between 0 and 100"), nil
|
||||
}
|
||||
|
||||
// Generate random number between 0-100
|
||||
randomValue := rand.Float64() * 100
|
||||
|
||||
if randomValue < args.FailRate {
|
||||
// Fail
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Intermittent failure (fail_rate: %.1f%%, random: %.2f)", args.FailRate, randomValue)), nil
|
||||
}
|
||||
|
||||
// Success
|
||||
response := map[string]interface{}{
|
||||
"success": true,
|
||||
"fail_rate": args.FailRate,
|
||||
"random": randomValue,
|
||||
"message": "Operation succeeded",
|
||||
}
|
||||
|
||||
jsonResult, _ := json.Marshal(response)
|
||||
return mcp.NewToolResultText(string(jsonResult)), nil
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 5: memory_intensive
|
||||
// ============================================================================
|
||||
|
||||
func registerMemoryIntensiveTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("memory_intensive",
|
||||
mcp.WithDescription("Allocates specified amount of memory to test resource limits"),
|
||||
mcp.WithNumber("size_mb",
|
||||
mcp.Required(),
|
||||
mcp.Description("Amount of memory to allocate in megabytes"),
|
||||
),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
var args struct {
|
||||
SizeMB int `json:"size_mb"`
|
||||
}
|
||||
|
||||
// Get arguments using the proper method
|
||||
argsInterface := request.GetArguments()
|
||||
|
||||
// Marshal and unmarshal to convert to our struct
|
||||
argsBytes, err := json.Marshal(argsInterface)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(argsBytes, &args); err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Invalid arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
// Limit to reasonable size to prevent crashes
|
||||
if args.SizeMB > 100 {
|
||||
return mcp.NewToolResultError("Size limited to 100MB for safety"), nil
|
||||
}
|
||||
|
||||
// Allocate memory (use int64 to prevent overflow)
|
||||
sizeBytes := int64(args.SizeMB) * 1024 * 1024
|
||||
data := make([]byte, sizeBytes)
|
||||
|
||||
// Fill with pattern to ensure allocation
|
||||
for i := range data {
|
||||
data[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
// Calculate checksum to verify allocation
|
||||
var checksum uint64
|
||||
for _, b := range data {
|
||||
checksum += uint64(b)
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"allocated_mb": args.SizeMB,
|
||||
"allocated_bytes": sizeBytes,
|
||||
"checksum": checksum,
|
||||
"message": fmt.Sprintf("Successfully allocated %dMB", args.SizeMB),
|
||||
}
|
||||
|
||||
// Clear memory before returning
|
||||
data = nil
|
||||
|
||||
jsonResult, _ := json.Marshal(response)
|
||||
return mcp.NewToolResultText(string(jsonResult)), nil
|
||||
})
|
||||
}
|
||||
1161
examples/mcps/error-test-server/package-lock.json
generated
Normal file
1161
examples/mcps/error-test-server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
examples/mcps/error-test-server/package.json
Normal file
24
examples/mcps/error-test-server/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "error-test-server",
|
||||
"version": "1.0.0",
|
||||
"description": "MCP STDIO server optimized for testing error scenarios and edge cases",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"error-test-server": "./dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc && chmod +x dist/index.js",
|
||||
"prepare": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"overrides": {
|
||||
"hono": "4.12.14"
|
||||
}
|
||||
}
|
||||
373
examples/mcps/error-test-server/src/index.ts
Normal file
373
examples/mcps/error-test-server/src/index.ts
Normal file
@@ -0,0 +1,373 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { z } from "zod";
|
||||
|
||||
// Schemas for error test tools
|
||||
const MalformedJsonSchema = z.object({
|
||||
id: z.string().describe("Tool invocation ID"),
|
||||
json_type: z.enum(["truncated", "invalid_escape", "unclosed_bracket", "mixed_types"]).optional(),
|
||||
});
|
||||
|
||||
const TimeoutToolSchema = z.object({
|
||||
id: z.string().describe("Tool invocation ID"),
|
||||
timeout_ms: z.number().optional().describe("Timeout duration in milliseconds (default 5000)"),
|
||||
});
|
||||
|
||||
const IntermittentFailSchema = z.object({
|
||||
id: z.string().describe("Tool invocation ID"),
|
||||
fail_rate: z.number().min(0).max(1).optional().describe("Probability of failure (0-1, default 0.5)"),
|
||||
});
|
||||
|
||||
const NetworkErrorSchema = z.object({
|
||||
id: z.string().describe("Tool invocation ID"),
|
||||
error_type: z.enum(["connection_refused", "timeout", "dns_failure", "ssl_error"]).optional(),
|
||||
});
|
||||
|
||||
const LargePayloadSchema = z.object({
|
||||
id: z.string().describe("Tool invocation ID"),
|
||||
size_kb: z.number().optional().describe("Payload size in KB (default 100)"),
|
||||
});
|
||||
|
||||
const PartialResponseSchema = z.object({
|
||||
id: z.string().describe("Tool invocation ID"),
|
||||
break_at: z.enum(["start", "middle", "end"]).optional().describe("Where to break the response"),
|
||||
});
|
||||
|
||||
const server = new Server(
|
||||
{ name: "error-test-server", version: "1.0.0" },
|
||||
{ capabilities: { tools: {} } }
|
||||
);
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [
|
||||
{
|
||||
name: "malformed_json",
|
||||
description: "Returns malformed JSON to test error handling",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
json_type: {
|
||||
type: "string",
|
||||
enum: ["truncated", "invalid_escape", "unclosed_bracket", "mixed_types"],
|
||||
description: "Type of JSON malformation",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "timeout_tool",
|
||||
description: "Hangs for a specified duration to test timeouts",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
timeout_ms: {
|
||||
type: "number",
|
||||
description: "Timeout duration in milliseconds (default 5000)",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "intermittent_fail",
|
||||
description: "Randomly fails to test retry logic",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
fail_rate: {
|
||||
type: "number",
|
||||
minimum: 0,
|
||||
maximum: 1,
|
||||
description: "Probability of failure (0-1, default 0.5)",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "network_error",
|
||||
description: "Simulates various network errors",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
error_type: {
|
||||
type: "string",
|
||||
enum: ["connection_refused", "timeout", "dns_failure", "ssl_error"],
|
||||
description: "Type of network error to simulate",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "large_payload",
|
||||
description: "Returns a very large payload to test size limits",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
size_kb: {
|
||||
type: "number",
|
||||
description: "Payload size in KB (default 100)",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "partial_response",
|
||||
description: "Returns incomplete response to test handling",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
break_at: {
|
||||
type: "string",
|
||||
enum: ["start", "middle", "end"],
|
||||
description: "Where to break the response",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid_content_type",
|
||||
description: "Returns content with mismatched type declaration",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const toolName = request.params.name;
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
switch (toolName) {
|
||||
case "malformed_json": {
|
||||
const args = MalformedJsonSchema.parse(request.params.arguments);
|
||||
const jsonType = args.json_type || "truncated";
|
||||
|
||||
let malformedText: string;
|
||||
switch (jsonType) {
|
||||
case "truncated":
|
||||
malformedText = '{"status": "success", "data": {"items": [1, 2, 3';
|
||||
break;
|
||||
case "invalid_escape":
|
||||
malformedText = '{"status": "success", "message": "Invalid \\x escape"}';
|
||||
break;
|
||||
case "unclosed_bracket":
|
||||
malformedText = '{"status": "success", "data": [1, 2, 3]';
|
||||
break;
|
||||
case "mixed_types":
|
||||
malformedText = '{"status": "success", "value": NaN, "other": undefined}';
|
||||
break;
|
||||
default:
|
||||
malformedText = '{"incomplete": true';
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: malformedText,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "timeout_tool": {
|
||||
const args = TimeoutToolSchema.parse(request.params.arguments);
|
||||
const timeoutMs = args.timeout_ms || 5000;
|
||||
|
||||
// Hang for the specified duration
|
||||
await new Promise((resolve) => setTimeout(resolve, timeoutMs));
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
tool: "timeout_tool",
|
||||
id: args.id,
|
||||
timeout_ms: timeoutMs,
|
||||
message: "This should have timed out",
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "intermittent_fail": {
|
||||
const args = IntermittentFailSchema.parse(request.params.arguments);
|
||||
const failRate = args.fail_rate ?? 0.5;
|
||||
|
||||
// Randomly fail based on fail_rate
|
||||
if (Math.random() < failRate) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
error: "Intermittent failure occurred",
|
||||
id: args.id,
|
||||
fail_rate: failRate,
|
||||
}),
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
tool: "intermittent_fail",
|
||||
id: args.id,
|
||||
success: true,
|
||||
fail_rate: failRate,
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "network_error": {
|
||||
const args = NetworkErrorSchema.parse(request.params.arguments);
|
||||
const errorType = args.error_type || "connection_refused";
|
||||
|
||||
const errorMessages = {
|
||||
connection_refused: "Connection refused: Unable to connect to remote server",
|
||||
timeout: "Request timeout: Server did not respond within timeout period",
|
||||
dns_failure: "DNS resolution failed: Unable to resolve hostname",
|
||||
ssl_error: "SSL handshake failed: Certificate verification error",
|
||||
};
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
error: errorMessages[errorType],
|
||||
error_type: errorType,
|
||||
id: args.id,
|
||||
}),
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
case "large_payload": {
|
||||
const args = LargePayloadSchema.parse(request.params.arguments);
|
||||
const sizeKb = args.size_kb || 100;
|
||||
|
||||
// Generate a large string (approximately sizeKb KB)
|
||||
const chunkSize = 1024; // 1 KB chunks
|
||||
const chunks: string[] = [];
|
||||
for (let i = 0; i < sizeKb; i++) {
|
||||
chunks.push("x".repeat(chunkSize));
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
tool: "large_payload",
|
||||
id: args.id,
|
||||
size_kb: sizeKb,
|
||||
payload: chunks.join(""),
|
||||
message: `Generated ${sizeKb}KB payload`,
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "partial_response": {
|
||||
const args = PartialResponseSchema.parse(request.params.arguments);
|
||||
const breakAt = args.break_at || "middle";
|
||||
|
||||
let response: string;
|
||||
switch (breakAt) {
|
||||
case "start":
|
||||
response = '{"sta';
|
||||
break;
|
||||
case "middle":
|
||||
response = '{"status": "success", "data": {"incomplete';
|
||||
break;
|
||||
case "end":
|
||||
response = '{"status": "success", "data": {"complete": true}, "message": "Almost done"';
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: response,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "invalid_content_type": {
|
||||
const args = z.object({ id: z.string() }).parse(request.params.arguments);
|
||||
|
||||
// Return a response that claims to be JSON but isn't properly formatted
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "This is not valid JSON content but the server says it is",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("Error Test MCP Server running on stdio");
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Fatal error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
17
examples/mcps/error-test-server/tsconfig.json
Normal file
17
examples/mcps/error-test-server/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
158
examples/mcps/go-test-server/README.md
Normal file
158
examples/mcps/go-test-server/README.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# Go Test Server
|
||||
|
||||
A test MCP server written in Go that provides string manipulation, JSON validation, UUID generation, hashing, and encoding/decoding tools.
|
||||
|
||||
## Tools
|
||||
|
||||
### 1. string_transform
|
||||
Performs string transformations.
|
||||
|
||||
**Parameters:**
|
||||
- `input` (string, required): The input string to transform
|
||||
- `operation` (string, required): Operation to perform - "uppercase", "lowercase", "reverse", "title"
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"input": "hello world",
|
||||
"operation": "uppercase"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"input": "hello world",
|
||||
"operation": "uppercase",
|
||||
"result": "HELLO WORLD"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. json_validate
|
||||
Validates if a string is valid JSON.
|
||||
|
||||
**Parameters:**
|
||||
- `json_string` (string, required): The JSON string to validate
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"json_string": "{\"name\": \"test\"}"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"valid": true,
|
||||
"parsed": {"name": "test"}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. uuid_generate
|
||||
Generates a random UUID v4.
|
||||
|
||||
**Parameters:** None
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"uuid": "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
```
|
||||
|
||||
### 4. hash
|
||||
Computes hash of input string.
|
||||
|
||||
**Parameters:**
|
||||
- `input` (string, required): The input string to hash
|
||||
- `algorithm` (string, required): Hash algorithm - "md5", "sha256", "sha512"
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"input": "hello",
|
||||
"algorithm": "sha256"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"input": "hello",
|
||||
"algorithm": "sha256",
|
||||
"hash": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
|
||||
}
|
||||
```
|
||||
|
||||
### 5. encode
|
||||
Encodes input string.
|
||||
|
||||
**Parameters:**
|
||||
- `input` (string, required): The input string to encode
|
||||
- `encoding` (string, required): Encoding type - "base64", "hex", "url"
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"input": "hello world",
|
||||
"encoding": "base64"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"input": "hello world",
|
||||
"encoding": "base64",
|
||||
"encoded": "aGVsbG8gd29ybGQ="
|
||||
}
|
||||
```
|
||||
|
||||
### 6. decode
|
||||
Decodes encoded string.
|
||||
|
||||
**Parameters:**
|
||||
- `input` (string, required): The encoded input string to decode
|
||||
- `encoding` (string, required): Encoding type - "base64", "hex", "url"
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"input": "aGVsbG8gd29ybGQ=",
|
||||
"encoding": "base64"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"input": "aGVsbG8gd29ybGQ=",
|
||||
"encoding": "base64",
|
||||
"decoded": "hello world"
|
||||
}
|
||||
```
|
||||
|
||||
## Build and Run
|
||||
|
||||
```bash
|
||||
# Build
|
||||
go build -o bin/go-test-server
|
||||
|
||||
# Run
|
||||
./bin/go-test-server
|
||||
```
|
||||
|
||||
## Usage in Tests
|
||||
|
||||
```go
|
||||
config := schemas.MCPClientConfig{
|
||||
ID: "go-test-server",
|
||||
Name: "GoTestServer",
|
||||
ConnectionType: schemas.MCPConnectionTypeSTDIO,
|
||||
StdioConfig: &schemas.MCPStdioConfig{
|
||||
Command: "/path/to/bin/go-test-server",
|
||||
Args: []string{},
|
||||
},
|
||||
}
|
||||
```
|
||||
19
examples/mcps/go-test-server/go.mod
Normal file
19
examples/mcps/go-test-server/go.mod
Normal file
@@ -0,0 +1,19 @@
|
||||
module github.com/maximhq/bifrost/examples/mcps/go-test-server
|
||||
|
||||
go 1.26.2
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mark3labs/mcp-go v0.43.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
39
examples/mcps/go-test-server/go.sum
Normal file
39
examples/mcps/go-test-server/go.sum
Normal file
@@ -0,0 +1,39 @@
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
||||
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I=
|
||||
github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
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=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
381
examples/mcps/go-test-server/main.go
Normal file
381
examples/mcps/go-test-server/main.go
Normal file
@@ -0,0 +1,381 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create MCP server
|
||||
s := server.NewMCPServer(
|
||||
"go-test-server",
|
||||
"1.0.0",
|
||||
server.WithToolCapabilities(true),
|
||||
)
|
||||
|
||||
// Register all tools
|
||||
registerStringTransformTool(s)
|
||||
registerJSONValidateTool(s)
|
||||
registerUUIDGenerateTool(s)
|
||||
registerHashTool(s)
|
||||
registerEncodeTool(s)
|
||||
registerDecodeTool(s)
|
||||
|
||||
// Start STDIO server
|
||||
if err := server.ServeStdio(s); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Server error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 1: string_transform
|
||||
// ============================================================================
|
||||
|
||||
func registerStringTransformTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("string_transform",
|
||||
mcp.WithDescription("Performs string transformations: uppercase, lowercase, reverse, title"),
|
||||
mcp.WithString("input",
|
||||
mcp.Required(),
|
||||
mcp.Description("The input string to transform"),
|
||||
),
|
||||
mcp.WithString("operation",
|
||||
mcp.Required(),
|
||||
mcp.Description("The operation to perform"),
|
||||
mcp.Enum("uppercase", "lowercase", "reverse", "title"),
|
||||
),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
var args struct {
|
||||
Input string `json:"input"`
|
||||
Operation string `json:"operation"`
|
||||
}
|
||||
|
||||
// Get arguments using the proper method
|
||||
argsInterface := request.GetArguments()
|
||||
|
||||
// Marshal and unmarshal to convert to our struct
|
||||
argsBytes, err := json.Marshal(argsInterface)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(argsBytes, &args); err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Invalid arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
var result string
|
||||
switch args.Operation {
|
||||
case "uppercase":
|
||||
result = strings.ToUpper(args.Input)
|
||||
case "lowercase":
|
||||
result = strings.ToLower(args.Input)
|
||||
case "reverse":
|
||||
runes := []rune(args.Input)
|
||||
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
|
||||
runes[i], runes[j] = runes[j], runes[i]
|
||||
}
|
||||
result = string(runes)
|
||||
case "title":
|
||||
result = strings.Title(strings.ToLower(args.Input))
|
||||
default:
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Unknown operation: %s", args.Operation)), nil
|
||||
}
|
||||
|
||||
response := map[string]string{
|
||||
"input": args.Input,
|
||||
"operation": args.Operation,
|
||||
"result": result,
|
||||
}
|
||||
|
||||
jsonResult, _ := json.Marshal(response)
|
||||
return mcp.NewToolResultText(string(jsonResult)), nil
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 2: json_validate
|
||||
// ============================================================================
|
||||
|
||||
func registerJSONValidateTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("json_validate",
|
||||
mcp.WithDescription("Validates if a string is valid JSON"),
|
||||
mcp.WithString("json_string",
|
||||
mcp.Required(),
|
||||
mcp.Description("The JSON string to validate"),
|
||||
),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
var args struct {
|
||||
JSONString string `json:"json_string"`
|
||||
}
|
||||
|
||||
// Get arguments using the proper method
|
||||
argsInterface := request.GetArguments()
|
||||
|
||||
// Marshal and unmarshal to convert to our struct
|
||||
argsBytes, err := json.Marshal(argsInterface)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(argsBytes, &args); err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Invalid arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
var jsonData interface{}
|
||||
err = json.Unmarshal([]byte(args.JSONString), &jsonData)
|
||||
|
||||
response := map[string]interface{}{
|
||||
"valid": err == nil,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
response["error"] = err.Error()
|
||||
} else {
|
||||
response["parsed"] = jsonData
|
||||
}
|
||||
|
||||
jsonResult, _ := json.Marshal(response)
|
||||
return mcp.NewToolResultText(string(jsonResult)), nil
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 3: uuid_generate
|
||||
// ============================================================================
|
||||
|
||||
func registerUUIDGenerateTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("uuid_generate",
|
||||
mcp.WithDescription("Generates a random UUID v4"),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
id := uuid.New()
|
||||
|
||||
response := map[string]string{
|
||||
"uuid": id.String(),
|
||||
}
|
||||
|
||||
jsonResult, _ := json.Marshal(response)
|
||||
return mcp.NewToolResultText(string(jsonResult)), nil
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 4: hash
|
||||
// ============================================================================
|
||||
|
||||
func registerHashTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("hash",
|
||||
mcp.WithDescription("Computes hash of input string using specified algorithm"),
|
||||
mcp.WithString("input",
|
||||
mcp.Required(),
|
||||
mcp.Description("The input string to hash"),
|
||||
),
|
||||
mcp.WithString("algorithm",
|
||||
mcp.Required(),
|
||||
mcp.Description("The hash algorithm to use"),
|
||||
mcp.Enum("md5", "sha256", "sha512"),
|
||||
),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
var args struct {
|
||||
Input string `json:"input"`
|
||||
Algorithm string `json:"algorithm"`
|
||||
}
|
||||
|
||||
// Get arguments using the proper method
|
||||
argsInterface := request.GetArguments()
|
||||
|
||||
// Marshal and unmarshal to convert to our struct
|
||||
argsBytes, err := json.Marshal(argsInterface)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(argsBytes, &args); err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Invalid arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
var hashResult string
|
||||
switch args.Algorithm {
|
||||
case "md5":
|
||||
hash := md5.Sum([]byte(args.Input))
|
||||
hashResult = hex.EncodeToString(hash[:])
|
||||
case "sha256":
|
||||
hash := sha256.Sum256([]byte(args.Input))
|
||||
hashResult = hex.EncodeToString(hash[:])
|
||||
case "sha512":
|
||||
hash := sha512.Sum512([]byte(args.Input))
|
||||
hashResult = hex.EncodeToString(hash[:])
|
||||
default:
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Unknown algorithm: %s", args.Algorithm)), nil
|
||||
}
|
||||
|
||||
response := map[string]string{
|
||||
"input": args.Input,
|
||||
"algorithm": args.Algorithm,
|
||||
"hash": hashResult,
|
||||
}
|
||||
|
||||
jsonResult, _ := json.Marshal(response)
|
||||
return mcp.NewToolResultText(string(jsonResult)), nil
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 5: encode
|
||||
// ============================================================================
|
||||
|
||||
func registerEncodeTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("encode",
|
||||
mcp.WithDescription("Encodes input string using specified encoding"),
|
||||
mcp.WithString("input",
|
||||
mcp.Required(),
|
||||
mcp.Description("The input string to encode"),
|
||||
),
|
||||
mcp.WithString("encoding",
|
||||
mcp.Required(),
|
||||
mcp.Description("The encoding to use"),
|
||||
mcp.Enum("base64", "hex", "url"),
|
||||
),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
var args struct {
|
||||
Input string `json:"input"`
|
||||
Encoding string `json:"encoding"`
|
||||
}
|
||||
|
||||
// Get arguments using the proper method
|
||||
argsInterface := request.GetArguments()
|
||||
|
||||
// Marshal and unmarshal to convert to our struct
|
||||
argsBytes, err := json.Marshal(argsInterface)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(argsBytes, &args); err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Invalid arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
var encoded string
|
||||
switch args.Encoding {
|
||||
case "base64":
|
||||
encoded = base64.StdEncoding.EncodeToString([]byte(args.Input))
|
||||
case "hex":
|
||||
encoded = hex.EncodeToString([]byte(args.Input))
|
||||
case "url":
|
||||
encoded = url.QueryEscape(args.Input)
|
||||
default:
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Unknown encoding: %s", args.Encoding)), nil
|
||||
}
|
||||
|
||||
response := map[string]string{
|
||||
"input": args.Input,
|
||||
"encoding": args.Encoding,
|
||||
"encoded": encoded,
|
||||
}
|
||||
|
||||
jsonResult, _ := json.Marshal(response)
|
||||
return mcp.NewToolResultText(string(jsonResult)), nil
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 6: decode
|
||||
// ============================================================================
|
||||
|
||||
func registerDecodeTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("decode",
|
||||
mcp.WithDescription("Decodes input string using specified encoding"),
|
||||
mcp.WithString("input",
|
||||
mcp.Required(),
|
||||
mcp.Description("The encoded input string to decode"),
|
||||
),
|
||||
mcp.WithString("encoding",
|
||||
mcp.Required(),
|
||||
mcp.Description("The encoding to use for decoding"),
|
||||
mcp.Enum("base64", "hex", "url"),
|
||||
),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
var args struct {
|
||||
Input string `json:"input"`
|
||||
Encoding string `json:"encoding"`
|
||||
}
|
||||
|
||||
// Get arguments using the proper method
|
||||
argsInterface := request.GetArguments()
|
||||
|
||||
// Marshal and unmarshal to convert to our struct
|
||||
argsBytes, err := json.Marshal(argsInterface)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(argsBytes, &args); err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Invalid arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
var decoded string
|
||||
var decodeErr error
|
||||
|
||||
switch args.Encoding {
|
||||
case "base64":
|
||||
decodedBytes, err := base64.StdEncoding.DecodeString(args.Input)
|
||||
if err != nil {
|
||||
decodeErr = err
|
||||
} else {
|
||||
decoded = string(decodedBytes)
|
||||
}
|
||||
case "hex":
|
||||
decodedBytes, err := hex.DecodeString(args.Input)
|
||||
if err != nil {
|
||||
decodeErr = err
|
||||
} else {
|
||||
decoded = string(decodedBytes)
|
||||
}
|
||||
case "url":
|
||||
var err error
|
||||
decoded, err = url.QueryUnescape(args.Input)
|
||||
if err != nil {
|
||||
decodeErr = err
|
||||
}
|
||||
default:
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Unknown encoding: %s", args.Encoding)), nil
|
||||
}
|
||||
|
||||
if decodeErr != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Decode error: %v", decodeErr)), nil
|
||||
}
|
||||
|
||||
response := map[string]string{
|
||||
"input": args.Input,
|
||||
"encoding": args.Encoding,
|
||||
"decoded": decoded,
|
||||
}
|
||||
|
||||
jsonResult, _ := json.Marshal(response)
|
||||
return mcp.NewToolResultText(string(jsonResult)), nil
|
||||
})
|
||||
}
|
||||
171
examples/mcps/http-no-ping-server/README.md
Normal file
171
examples/mcps/http-no-ping-server/README.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# HTTP MCP Server Without Ping Support
|
||||
|
||||
This is a sample MCP server implementation that runs over HTTP but **does not support the optional `ping` method**. This demonstrates how to configure Bifrost to use the `listTools` health check method instead of ping.
|
||||
|
||||
## What is This?
|
||||
|
||||
Many MCP servers may not implement the optional `ping` method from the MCP specification. This example shows:
|
||||
|
||||
1. **How to build an MCP server** that only supports the core methods (`list_tools`, `call_tool`) but not `ping`
|
||||
2. **How to configure Bifrost** to work with such servers using `is_ping_available: false`
|
||||
3. **Why this matters**: When `is_ping_available` is `false`, Bifrost will use `listTools` for health checks instead of the lightweight `ping` method
|
||||
|
||||
## Running the Server
|
||||
|
||||
### Prerequisites
|
||||
|
||||
```bash
|
||||
go 1.26.1+
|
||||
```
|
||||
|
||||
### Start the Server
|
||||
|
||||
```bash
|
||||
# From this directory
|
||||
go run main.go
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
MCP server listening on http://localhost:3001/mcp
|
||||
Note: This server does NOT support ping. Use is_ping_available=false in Bifrost config.
|
||||
```
|
||||
|
||||
## Connecting via Bifrost
|
||||
|
||||
### Configuration (config.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"client_configs": [
|
||||
{
|
||||
"name": "http_no_ping_server",
|
||||
"connection_type": "http",
|
||||
"connection_string": "http://localhost:3001/mcp",
|
||||
"is_ping_available": false,
|
||||
"tools_to_execute": ["*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Via API
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/mcp/client \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "http_no_ping_server",
|
||||
"connection_type": "http",
|
||||
"connection_string": "http://localhost:3001/mcp",
|
||||
"is_ping_available": false,
|
||||
"tools_to_execute": ["*"]
|
||||
}'
|
||||
```
|
||||
|
||||
### Via Web UI
|
||||
|
||||
1. Navigate to **MCP Gateway**
|
||||
2. Click **New MCP Server**
|
||||
3. Fill in:
|
||||
- **Name**: `http_no_ping_server`
|
||||
- **Connection Type**: HTTP
|
||||
- **Connection URL**: `http://localhost:3001/mcp`
|
||||
- **Ping Available for Health Check**: Toggle OFF (disabled)
|
||||
4. Click **Create**
|
||||
|
||||
## Available Tools
|
||||
|
||||
This server provides three simple tools for testing:
|
||||
|
||||
### 1. echo
|
||||
Echoes back the input message.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "echo",
|
||||
"arguments": {
|
||||
"message": "Hello, World!"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. add
|
||||
Adds two numbers together.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "add",
|
||||
"arguments": {
|
||||
"a": 5,
|
||||
"b": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. greet
|
||||
Greets someone by name.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "greet",
|
||||
"arguments": {
|
||||
"name": "Alice"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Health Check Behavior
|
||||
|
||||
When you add this server to Bifrost with `is_ping_available: false`:
|
||||
|
||||
1. Bifrost will **NOT** send `ping` requests (since the server doesn't support them)
|
||||
2. Instead, Bifrost will use `listTools` every 10 seconds to check server health
|
||||
3. If `listTools` fails 5 consecutive times, the server will be marked as `disconnected`
|
||||
|
||||
**Why `listTools` instead of `ping`?**
|
||||
- `ping` is lighter and faster, but optional in MCP
|
||||
- `listTools` is heavier but guaranteed to exist on all MCP servers
|
||||
- Using `listTools` for health checks is a fallback for servers without `ping` support
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
This example intentionally:
|
||||
|
||||
- ✅ Supports all core MCP methods (list_tools, call_tool)
|
||||
- ✅ Returns proper JSON-RPC responses
|
||||
- ✅ Works over HTTP
|
||||
- ❌ Does NOT implement the `ping` method
|
||||
- ❌ Returns a JSON-RPC method-not-found error (-32601) when ping is attempted
|
||||
|
||||
### How Ping is Blocked
|
||||
|
||||
The mcp-go library's `NewStreamableHTTPServer` automatically includes ping support by default. To demonstrate a server without ping, this example uses **HTTP middleware** that:
|
||||
|
||||
1. Intercepts all POST requests
|
||||
2. Checks if the request is a `ping` method call
|
||||
3. If it's a ping request, returns a JSON-RPC error: `{"code": -32601, "message": "Method not found: ping is not supported by this server"}`
|
||||
4. For all other requests (list_tools, call_tool), passes them through normally
|
||||
|
||||
This allows us to:
|
||||
- ✅ Keep the simple mcp-go server implementation
|
||||
- ✅ Transparently block ping requests at the HTTP layer
|
||||
- ✅ Return proper JSON-RPC error responses
|
||||
- ✅ Demonstrate the `is_ping_available=false` behavior in Bifrost
|
||||
|
||||
## Key Learning: is_ping_available
|
||||
|
||||
The `is_ping_available` setting is important because:
|
||||
|
||||
| Setting | Health Check Method | When to Use |
|
||||
|---------|-------------------|-----------|
|
||||
| `true` (default) | Lightweight `ping` | When your server supports ping (recommended) |
|
||||
| `false` | Heavier `listTools` | When your server doesn't support ping |
|
||||
|
||||
## See Also
|
||||
|
||||
- [MCP Specification](https://spec.modelcontextprotocol.io/)
|
||||
- [Bifrost MCP Documentation](../../docs/mcp/connecting-to-servers.mdx)
|
||||
- [Health Monitoring Guide](../../docs/mcp/connecting-to-servers.mdx#health-monitoring)
|
||||
17
examples/mcps/http-no-ping-server/go.mod
Normal file
17
examples/mcps/http-no-ping-server/go.mod
Normal file
@@ -0,0 +1,17 @@
|
||||
module http-no-ping-server
|
||||
|
||||
go 1.26.2
|
||||
|
||||
require github.com/mark3labs/mcp-go v0.43.2
|
||||
|
||||
require (
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
39
examples/mcps/http-no-ping-server/go.sum
Normal file
39
examples/mcps/http-no-ping-server/go.sum
Normal file
@@ -0,0 +1,39 @@
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
||||
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I=
|
||||
github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
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=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
194
examples/mcps/http-no-ping-server/main.go
Normal file
194
examples/mcps/http-no-ping-server/main.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
// NoPingMCPServer is an HTTP MCP server that intentionally does not support ping.
|
||||
// This demonstrates how to configure servers with is_ping_available=false in Bifrost
|
||||
// when your MCP server implementation doesn't support the optional ping method.
|
||||
func main() {
|
||||
// Create MCP server
|
||||
mcpServer := server.NewMCPServer(
|
||||
"http-no-ping-server",
|
||||
"1.0.0",
|
||||
)
|
||||
|
||||
// Define tools using the proper NewTool API
|
||||
echoTool := mcp.NewTool(
|
||||
"echo",
|
||||
mcp.WithDescription("Echo back the input message"),
|
||||
mcp.WithString("message", mcp.Required(), mcp.Description("Message to echo")),
|
||||
)
|
||||
|
||||
addTool := mcp.NewTool(
|
||||
"add",
|
||||
mcp.WithDescription("Add two numbers"),
|
||||
mcp.WithNumber("a", mcp.Required(), mcp.Description("First number")),
|
||||
mcp.WithNumber("b", mcp.Required(), mcp.Description("Second number")),
|
||||
)
|
||||
|
||||
greetTool := mcp.NewTool(
|
||||
"greet",
|
||||
mcp.WithDescription("Greet someone by name"),
|
||||
mcp.WithString("name", mcp.Required(), mcp.Description("Name to greet")),
|
||||
)
|
||||
|
||||
// Register tool handlers
|
||||
mcpServer.AddTool(echoTool, echoHandler)
|
||||
mcpServer.AddTool(addTool, addHandler)
|
||||
mcpServer.AddTool(greetTool, greetHandler)
|
||||
|
||||
// Create HTTP server using StreamableHTTP transport
|
||||
httpServer := server.NewStreamableHTTPServer(mcpServer)
|
||||
|
||||
port := 3001
|
||||
addr := fmt.Sprintf("localhost:%d", port)
|
||||
|
||||
log.Printf("MCP server listening on http://%s/", addr)
|
||||
log.Printf("Note: This server does NOT support ping. Use is_ping_available=false in Bifrost config.")
|
||||
log.Printf("\nExample Bifrost config:")
|
||||
log.Printf(`
|
||||
{
|
||||
"name": "http_no_ping_server",
|
||||
"connection_type": "http",
|
||||
"connection_string": "http://%s/",
|
||||
"is_ping_available": false,
|
||||
"tools_to_execute": ["*"]
|
||||
}
|
||||
`, addr)
|
||||
|
||||
// Wrap the HTTP server with middleware that rejects ping requests
|
||||
wrappedHandler := noPingMiddleware(httpServer)
|
||||
|
||||
if err := http.ListenAndServe(addr, wrappedHandler); err != nil {
|
||||
log.Fatalf("Server error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// echoHandler handles the echo tool
|
||||
func echoHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
// Extract arguments as JSON
|
||||
var args struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Parse the arguments
|
||||
argBytes, err := json.Marshal(request.Params.Arguments)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to parse arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(argBytes, &args); err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Invalid arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("Echo: %s", args.Message)
|
||||
return mcp.NewToolResultText(result), nil
|
||||
}
|
||||
|
||||
// addHandler handles the add tool
|
||||
func addHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
// Extract arguments as JSON
|
||||
var args struct {
|
||||
A float64 `json:"a"`
|
||||
B float64 `json:"b"`
|
||||
}
|
||||
|
||||
// Parse the arguments
|
||||
argBytes, err := json.Marshal(request.Params.Arguments)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to parse arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(argBytes, &args); err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Invalid arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
result := args.A + args.B
|
||||
return mcp.NewToolResultText(fmt.Sprintf("%v + %v = %v", args.A, args.B, result)), nil
|
||||
}
|
||||
|
||||
// greetHandler handles the greet tool
|
||||
func greetHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
// Extract arguments as JSON
|
||||
var args struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Parse the arguments
|
||||
argBytes, err := json.Marshal(request.Params.Arguments)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to parse arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(argBytes, &args); err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Invalid arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("Hello, %s! Welcome to the MCP server.", args.Name)
|
||||
return mcp.NewToolResultText(result), nil
|
||||
}
|
||||
|
||||
// noPingMiddleware is HTTP middleware that rejects ping requests
|
||||
// This allows us to demonstrate a server that doesn't support ping
|
||||
func noPingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Only intercept POST requests (MCP messages)
|
||||
if r.Method != http.MethodPost {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Read the request body
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to read request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the JSON-RPC request to check if it's a ping request
|
||||
var jsonRequest map[string]interface{}
|
||||
if err := json.Unmarshal(body, &jsonRequest); err != nil {
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this is a ping request
|
||||
if method, ok := jsonRequest["method"].(string); ok && method == "ping" {
|
||||
// Reject ping requests with a method not found error
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
var id interface{}
|
||||
if idVal, ok := jsonRequest["id"]; ok {
|
||||
id = idVal
|
||||
}
|
||||
|
||||
errorResponse := map[string]interface{}{
|
||||
"jsonrpc": "2.0",
|
||||
"error": map[string]interface{}{
|
||||
"code": -32601,
|
||||
"message": "Method not found: ping is not supported by this server",
|
||||
},
|
||||
"id": id,
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(errorResponse)
|
||||
return
|
||||
}
|
||||
|
||||
// For non-ping requests, restore the body and pass through to the next handler
|
||||
r.Body = io.NopCloser(strings.NewReader(string(body)))
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
17
examples/mcps/parallel-test-server/go.mod
Normal file
17
examples/mcps/parallel-test-server/go.mod
Normal file
@@ -0,0 +1,17 @@
|
||||
module github.com/maximhq/bifrost/examples/mcps/parallel-test-server
|
||||
|
||||
go 1.26.2
|
||||
|
||||
require github.com/mark3labs/mcp-go v0.43.2
|
||||
|
||||
require (
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
39
examples/mcps/parallel-test-server/go.sum
Normal file
39
examples/mcps/parallel-test-server/go.sum
Normal file
@@ -0,0 +1,39 @@
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
||||
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I=
|
||||
github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
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=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
172
examples/mcps/parallel-test-server/main.go
Normal file
172
examples/mcps/parallel-test-server/main.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create MCP server
|
||||
s := server.NewMCPServer(
|
||||
"parallel-test-server",
|
||||
"1.0.0",
|
||||
server.WithToolCapabilities(true),
|
||||
)
|
||||
|
||||
// Register all tools
|
||||
registerFastOperationTool(s)
|
||||
registerMediumOperationTool(s)
|
||||
registerSlowOperationTool(s)
|
||||
registerVerySlowOperationTool(s)
|
||||
registerReturnTimestampTool(s)
|
||||
|
||||
// Start STDIO server
|
||||
if err := server.ServeStdio(s); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Server error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 1: fast_operation
|
||||
// ============================================================================
|
||||
|
||||
func registerFastOperationTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("fast_operation",
|
||||
mcp.WithDescription("Returns immediately (< 10ms)"),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
start := time.Now()
|
||||
|
||||
response := map[string]interface{}{
|
||||
"operation": "fast",
|
||||
"timestamp": start.UnixNano(),
|
||||
"message": "Fast operation completed",
|
||||
}
|
||||
|
||||
elapsed := time.Since(start)
|
||||
response["elapsed_ms"] = float64(elapsed.Nanoseconds()) / 1e6
|
||||
|
||||
jsonResult, _ := json.Marshal(response)
|
||||
return mcp.NewToolResultText(string(jsonResult)), nil
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 2: medium_operation
|
||||
// ============================================================================
|
||||
|
||||
func registerMediumOperationTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("medium_operation",
|
||||
mcp.WithDescription("Takes 100-200ms to complete"),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
start := time.Now()
|
||||
|
||||
// Sleep for 150ms
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
|
||||
response := map[string]interface{}{
|
||||
"operation": "medium",
|
||||
"timestamp": start.UnixNano(),
|
||||
"message": "Medium operation completed",
|
||||
}
|
||||
|
||||
elapsed := time.Since(start)
|
||||
response["elapsed_ms"] = float64(elapsed.Nanoseconds()) / 1e6
|
||||
|
||||
jsonResult, _ := json.Marshal(response)
|
||||
return mcp.NewToolResultText(string(jsonResult)), nil
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 3: slow_operation
|
||||
// ============================================================================
|
||||
|
||||
func registerSlowOperationTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("slow_operation",
|
||||
mcp.WithDescription("Takes 500-1000ms to complete"),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
start := time.Now()
|
||||
|
||||
// Sleep for 750ms
|
||||
time.Sleep(750 * time.Millisecond)
|
||||
|
||||
response := map[string]interface{}{
|
||||
"operation": "slow",
|
||||
"timestamp": start.UnixNano(),
|
||||
"message": "Slow operation completed",
|
||||
}
|
||||
|
||||
elapsed := time.Since(start)
|
||||
response["elapsed_ms"] = float64(elapsed.Nanoseconds()) / 1e6
|
||||
|
||||
jsonResult, _ := json.Marshal(response)
|
||||
return mcp.NewToolResultText(string(jsonResult)), nil
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 4: very_slow_operation
|
||||
// ============================================================================
|
||||
|
||||
func registerVerySlowOperationTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("very_slow_operation",
|
||||
mcp.WithDescription("Takes 2-3 seconds to complete"),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
start := time.Now()
|
||||
|
||||
// Sleep for 2.5 seconds
|
||||
time.Sleep(2500 * time.Millisecond)
|
||||
|
||||
response := map[string]interface{}{
|
||||
"operation": "very_slow",
|
||||
"timestamp": start.UnixNano(),
|
||||
"message": "Very slow operation completed",
|
||||
}
|
||||
|
||||
elapsed := time.Since(start)
|
||||
response["elapsed_ms"] = float64(elapsed.Nanoseconds()) / 1e6
|
||||
|
||||
jsonResult, _ := json.Marshal(response)
|
||||
return mcp.NewToolResultText(string(jsonResult)), nil
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 5: return_timestamp
|
||||
// ============================================================================
|
||||
|
||||
func registerReturnTimestampTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("return_timestamp",
|
||||
mcp.WithDescription("Returns high-precision timestamp immediately"),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
now := time.Now()
|
||||
|
||||
response := map[string]interface{}{
|
||||
"timestamp_unix": now.Unix(),
|
||||
"timestamp_unix_nano": now.UnixNano(),
|
||||
"timestamp_unix_micro": now.UnixMicro(),
|
||||
"timestamp_iso8601": now.Format(time.RFC3339Nano),
|
||||
"message": "Timestamp captured",
|
||||
}
|
||||
|
||||
jsonResult, _ := json.Marshal(response)
|
||||
return mcp.NewToolResultText(string(jsonResult)), nil
|
||||
})
|
||||
}
|
||||
1161
examples/mcps/parallel-test-server/package-lock.json
generated
Normal file
1161
examples/mcps/parallel-test-server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
examples/mcps/parallel-test-server/package.json
Normal file
24
examples/mcps/parallel-test-server/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "parallel-test-server",
|
||||
"version": "1.0.0",
|
||||
"description": "MCP STDIO server optimized for testing parallel tool execution",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"parallel-test-server": "./dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc && chmod +x dist/index.js",
|
||||
"prepare": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"overrides": {
|
||||
"hono": "4.12.14"
|
||||
}
|
||||
}
|
||||
177
examples/mcps/parallel-test-server/src/index.ts
Normal file
177
examples/mcps/parallel-test-server/src/index.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { z } from "zod";
|
||||
|
||||
// Schemas for parallel test tools
|
||||
const FastToolSchema = z.object({
|
||||
id: z.string().describe("Tool invocation ID"),
|
||||
});
|
||||
|
||||
const SlowToolSchema = z.object({
|
||||
id: z.string().describe("Tool invocation ID"),
|
||||
delay_ms: z.number().optional().describe("Delay in milliseconds (default 100)"),
|
||||
});
|
||||
|
||||
const server = new Server(
|
||||
{ name: "parallel-test-server", version: "1.0.0" },
|
||||
{ capabilities: { tools: {} } }
|
||||
);
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [
|
||||
{
|
||||
name: "fast_tool_1",
|
||||
description: "Fast tool (10ms delay)",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: { id: { type: "string" } },
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fast_tool_2",
|
||||
description: "Fast tool (20ms delay)",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: { id: { type: "string" } },
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "medium_tool_1",
|
||||
description: "Medium tool (50ms delay)",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: { id: { type: "string" } },
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "medium_tool_2",
|
||||
description: "Medium tool (75ms delay)",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: { id: { type: "string" } },
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "slow_tool_1",
|
||||
description: "Slow tool (100ms delay)",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: { id: { type: "string" } },
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "slow_tool_2",
|
||||
description: "Slow tool (150ms delay)",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: { id: { type: "string" } },
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "variable_delay",
|
||||
description: "Tool with configurable delay",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
delay_ms: { type: "number", description: "Delay in milliseconds" },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const toolName = request.params.name;
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
let delay = 0;
|
||||
let args: any;
|
||||
|
||||
switch (toolName) {
|
||||
case "fast_tool_1":
|
||||
args = FastToolSchema.parse(request.params.arguments);
|
||||
delay = 10;
|
||||
break;
|
||||
case "fast_tool_2":
|
||||
args = FastToolSchema.parse(request.params.arguments);
|
||||
delay = 20;
|
||||
break;
|
||||
case "medium_tool_1":
|
||||
args = FastToolSchema.parse(request.params.arguments);
|
||||
delay = 50;
|
||||
break;
|
||||
case "medium_tool_2":
|
||||
args = FastToolSchema.parse(request.params.arguments);
|
||||
delay = 75;
|
||||
break;
|
||||
case "slow_tool_1":
|
||||
args = FastToolSchema.parse(request.params.arguments);
|
||||
delay = 100;
|
||||
break;
|
||||
case "slow_tool_2":
|
||||
args = FastToolSchema.parse(request.params.arguments);
|
||||
delay = 150;
|
||||
break;
|
||||
case "variable_delay":
|
||||
args = SlowToolSchema.parse(request.params.arguments);
|
||||
delay = args.delay_ms || 100;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
const elapsed = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
tool: toolName,
|
||||
id: args.id,
|
||||
delay_ms: delay,
|
||||
actual_elapsed_ms: elapsed,
|
||||
completed_at: new Date().toISOString(),
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("Parallel Test MCP Server running on stdio");
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Fatal error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
17
examples/mcps/parallel-test-server/tsconfig.json
Normal file
17
examples/mcps/parallel-test-server/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
105
examples/mcps/temperature/README.md
Normal file
105
examples/mcps/temperature/README.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# Temperature MCP Server
|
||||
|
||||
A simple Model Context Protocol (MCP) server that provides temperature information for popular cities around the world. This server exposes a single tool `get_temperature` that returns dummy temperature data for demonstration purposes.
|
||||
|
||||
## Features
|
||||
|
||||
- Single MCP tool: `get_temperature`
|
||||
- Supports 20+ popular cities worldwide
|
||||
- Returns temperature in Celsius or Fahrenheit
|
||||
- Includes weather conditions
|
||||
- Uses dummy/mock data (no external API calls)
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Running the Server
|
||||
|
||||
The server runs on stdio transport (standard input/output) by default:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
### Using with MCP Clients
|
||||
|
||||
This server can be used with any MCP-compatible client. Add it to your client configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"temperature": {
|
||||
"command": "node",
|
||||
"args": ["/path/to/temperature-mcp/dist/index.js"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Available Tool
|
||||
|
||||
#### get_temperature
|
||||
|
||||
Get the current temperature for a popular city.
|
||||
|
||||
**Input:**
|
||||
- `location` (string, required): The name of the city
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"location": "New York"
|
||||
}
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
Temperature in New York: 72°F
|
||||
Condition: Partly Cloudy
|
||||
```
|
||||
|
||||
## Supported Cities
|
||||
|
||||
The server provides temperature data for the following cities:
|
||||
|
||||
- New York, Los Angeles, San Francisco, Chicago (USA)
|
||||
- London, Paris, Berlin, Moscow (Europe)
|
||||
- Tokyo, Beijing, Shanghai, Hong Kong, Seoul, Singapore (Asia)
|
||||
- Sydney (Australia)
|
||||
- Dubai (Middle East)
|
||||
- Mumbai (India)
|
||||
- Toronto (Canada)
|
||||
- Mexico City (Mexico)
|
||||
- Rio de Janeiro (Brazil)
|
||||
|
||||
## Development
|
||||
|
||||
To run in development mode:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
This server demonstrates:
|
||||
- TypeScript MCP server implementation
|
||||
- Tool registration and execution
|
||||
- Input validation using Zod
|
||||
- Stdio transport for communication
|
||||
- Error handling and user-friendly messages
|
||||
|
||||
## Note
|
||||
|
||||
This server uses dummy data for demonstration purposes. In a production environment, you would integrate with a real weather API service.
|
||||
1158
examples/mcps/temperature/package-lock.json
generated
Normal file
1158
examples/mcps/temperature/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
examples/mcps/temperature/package.json
Normal file
23
examples/mcps/temperature/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "temperature-mcp-server",
|
||||
"version": "1.0.0",
|
||||
"description": "A simple MCP server that provides temperature information for popular locations",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "tsc && node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||
"zod": "^3.25.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"overrides": {
|
||||
"hono": "4.12.14"
|
||||
}
|
||||
}
|
||||
473
examples/mcps/temperature/src/index.ts
Normal file
473
examples/mcps/temperature/src/index.ts
Normal file
@@ -0,0 +1,473 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { z } from "zod";
|
||||
import * as fs from "fs";
|
||||
|
||||
// Dummy temperature data for popular locations
|
||||
const TEMPERATURE_DATA: Record<string, { temperature: number; unit: string; condition: string }> = {
|
||||
"new york": { temperature: 72, unit: "F", condition: "Partly Cloudy" },
|
||||
"london": { temperature: 15, unit: "C", condition: "Rainy" },
|
||||
"tokyo": { temperature: 22, unit: "C", condition: "Clear" },
|
||||
"paris": { temperature: 18, unit: "C", condition: "Cloudy" },
|
||||
"sydney": { temperature: 25, unit: "C", condition: "Sunny" },
|
||||
"dubai": { temperature: 35, unit: "C", condition: "Hot and Sunny" },
|
||||
"singapore": { temperature: 30, unit: "C", condition: "Humid" },
|
||||
"mumbai": { temperature: 32, unit: "C", condition: "Humid and Partly Cloudy" },
|
||||
"los angeles": { temperature: 75, unit: "F", condition: "Sunny" },
|
||||
"san francisco": { temperature: 62, unit: "F", condition: "Foggy" },
|
||||
"chicago": { temperature: 68, unit: "F", condition: "Windy" },
|
||||
"toronto": { temperature: 18, unit: "C", condition: "Clear" },
|
||||
"berlin": { temperature: 16, unit: "C", condition: "Cloudy" },
|
||||
"moscow": { temperature: 10, unit: "C", condition: "Cold" },
|
||||
"beijing": { temperature: 20, unit: "C", condition: "Clear" },
|
||||
"shanghai": { temperature: 24, unit: "C", condition: "Partly Cloudy" },
|
||||
"hong kong": { temperature: 28, unit: "C", condition: "Humid" },
|
||||
"seoul": { temperature: 19, unit: "C", condition: "Clear" },
|
||||
"mexico city": { temperature: 22, unit: "C", condition: "Sunny" },
|
||||
"rio de janeiro": { temperature: 28, unit: "C", condition: "Tropical" },
|
||||
};
|
||||
|
||||
// Tool input schemas
|
||||
const GetTemperatureSchema = z.object({
|
||||
location: z.string().describe("The name of the city (e.g., 'New York', 'London', 'Tokyo')"),
|
||||
});
|
||||
|
||||
const EchoSchema = z.object({
|
||||
text: z.string().describe("The text to echo back"),
|
||||
});
|
||||
|
||||
const CalculatorSchema = z.object({
|
||||
operation: z.enum(["add", "subtract", "multiply", "divide"]).describe("The operation to perform"),
|
||||
x: z.number().describe("First number"),
|
||||
y: z.number().describe("Second number"),
|
||||
});
|
||||
|
||||
const GetWeatherSchema = z.object({
|
||||
location: z.string().describe("The location to get weather for"),
|
||||
});
|
||||
|
||||
const SearchSchema = z.object({
|
||||
query: z.string().describe("The search query"),
|
||||
});
|
||||
|
||||
const GetTimeSchema = z.object({
|
||||
timezone: z.string().optional().describe("The timezone (optional)"),
|
||||
});
|
||||
|
||||
const ReadFileSchema = z.object({
|
||||
path: z.string().describe("The file path to read"),
|
||||
});
|
||||
|
||||
const DelaySchema = z.object({
|
||||
seconds: z.number().describe("Number of seconds to delay"),
|
||||
});
|
||||
|
||||
const ThrowErrorSchema = z.object({
|
||||
error_message: z.string().describe("The error message to throw"),
|
||||
});
|
||||
|
||||
// Create the MCP server
|
||||
const server = new Server(
|
||||
{
|
||||
name: "temperature-server",
|
||||
version: "1.0.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Handler for listing available tools
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: [
|
||||
{
|
||||
name: "get_temperature",
|
||||
description: "Get the current temperature for a popular city. Supports major cities worldwide.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
location: {
|
||||
type: "string",
|
||||
description: "The name of the city (e.g., 'New York', 'London', 'Tokyo')",
|
||||
},
|
||||
},
|
||||
required: ["location"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "echo",
|
||||
description: "Echoes back the provided text",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
text: {
|
||||
type: "string",
|
||||
description: "The text to echo back",
|
||||
},
|
||||
},
|
||||
required: ["text"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "calculator",
|
||||
description: "Performs basic arithmetic operations",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
operation: {
|
||||
type: "string",
|
||||
enum: ["add", "subtract", "multiply", "divide"],
|
||||
description: "The operation to perform",
|
||||
},
|
||||
x: {
|
||||
type: "number",
|
||||
description: "First number",
|
||||
},
|
||||
y: {
|
||||
type: "number",
|
||||
description: "Second number",
|
||||
},
|
||||
},
|
||||
required: ["operation", "x", "y"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_weather",
|
||||
description: "Get weather information for a location (alias for get_temperature)",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
location: {
|
||||
type: "string",
|
||||
description: "The location to get weather for",
|
||||
},
|
||||
},
|
||||
required: ["location"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "search",
|
||||
description: "Performs a search operation",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: {
|
||||
type: "string",
|
||||
description: "The search query",
|
||||
},
|
||||
},
|
||||
required: ["query"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_time",
|
||||
description: "Gets the current time",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
timezone: {
|
||||
type: "string",
|
||||
description: "The timezone (optional)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "read_file",
|
||||
description: "Reads a file from the filesystem",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: {
|
||||
type: "string",
|
||||
description: "The file path to read",
|
||||
},
|
||||
},
|
||||
required: ["path"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "delay",
|
||||
description: "Delays execution for specified seconds",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
seconds: {
|
||||
type: "number",
|
||||
description: "Number of seconds to delay",
|
||||
},
|
||||
},
|
||||
required: ["seconds"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "throw_error",
|
||||
description: "Throws an error with specified message",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
error_message: {
|
||||
type: "string",
|
||||
description: "The error message to throw",
|
||||
},
|
||||
},
|
||||
required: ["error_message"],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
// Handler for tool execution
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
try {
|
||||
const toolName = request.params.name;
|
||||
|
||||
switch (toolName) {
|
||||
case "get_temperature": {
|
||||
const args = GetTemperatureSchema.parse(request.params.arguments);
|
||||
const locationKey = args.location.toLowerCase();
|
||||
|
||||
if (!(locationKey in TEMPERATURE_DATA)) {
|
||||
const availableCities = Object.keys(TEMPERATURE_DATA)
|
||||
.map((city) => city.charAt(0).toUpperCase() + city.slice(1))
|
||||
.join(", ");
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Sorry, temperature data is not available for "${args.location}". Available cities: ${availableCities}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const data = TEMPERATURE_DATA[locationKey];
|
||||
const locationDisplay = args.location.charAt(0).toUpperCase() + args.location.slice(1);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Temperature in ${locationDisplay}: ${data.temperature}°${data.unit}\nCondition: ${data.condition}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "echo": {
|
||||
const args = EchoSchema.parse(request.params.arguments);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({ text: args.text }),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "calculator": {
|
||||
const args = CalculatorSchema.parse(request.params.arguments);
|
||||
let result: number;
|
||||
|
||||
switch (args.operation) {
|
||||
case "add":
|
||||
result = args.x + args.y;
|
||||
break;
|
||||
case "subtract":
|
||||
result = args.x - args.y;
|
||||
break;
|
||||
case "multiply":
|
||||
result = args.x * args.y;
|
||||
break;
|
||||
case "divide":
|
||||
if (args.y === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Error: Division by zero",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
result = args.x / args.y;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({ result }),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "get_weather": {
|
||||
// Alias for get_temperature
|
||||
const args = GetWeatherSchema.parse(request.params.arguments);
|
||||
const locationKey = args.location.toLowerCase();
|
||||
|
||||
if (!(locationKey in TEMPERATURE_DATA)) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Weather data not available for "${args.location}"`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const data = TEMPERATURE_DATA[locationKey];
|
||||
const locationDisplay = args.location.charAt(0).toUpperCase() + args.location.slice(1);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
location: locationDisplay,
|
||||
temperature: data.temperature,
|
||||
unit: data.unit,
|
||||
condition: data.condition,
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "search": {
|
||||
const args = SearchSchema.parse(request.params.arguments);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
query: args.query,
|
||||
results: [`Result 1 for ${args.query}`, `Result 2 for ${args.query}`],
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "get_time": {
|
||||
const args = GetTimeSchema.parse(request.params.arguments);
|
||||
const currentTime = new Date();
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
time: currentTime.toISOString(),
|
||||
timezone: args.timezone || "UTC",
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "read_file": {
|
||||
const args = ReadFileSchema.parse(request.params.arguments);
|
||||
try {
|
||||
const content = fs.readFileSync(args.path, "utf-8");
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({ path: args.path, content }),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (fileError) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error reading file: ${fileError instanceof Error ? fileError.message : String(fileError)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
case "delay": {
|
||||
const args = DelaySchema.parse(request.params.arguments);
|
||||
await new Promise((resolve) => setTimeout(resolve, args.seconds * 1000));
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
delayed_seconds: args.seconds,
|
||||
message: `Delayed for ${args.seconds} seconds`,
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "throw_error": {
|
||||
const args = ThrowErrorSchema.parse(request.params.arguments);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: args.error_message,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error in tool execution:`, error);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Start the server with stdio transport
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
|
||||
// Keep process alive - stdin will keep the process running
|
||||
// The process will exit when stdin is closed by the parent
|
||||
process.stdin.resume();
|
||||
|
||||
console.error("Temperature MCP Server running on stdio");
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Fatal error in main():", error);
|
||||
process.exit(1);
|
||||
});
|
||||
18
examples/mcps/temperature/tsconfig.json
Normal file
18
examples/mcps/temperature/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
28
examples/mcps/test-tools-server/README.md
Normal file
28
examples/mcps/test-tools-server/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Test Tools MCP Server
|
||||
|
||||
Standard MCP STDIO server with common test tools for integration testing.
|
||||
|
||||
## Tools
|
||||
|
||||
- **echo** - Echoes back a message
|
||||
- **calculator** - Basic arithmetic operations (add, subtract, multiply, divide)
|
||||
- **get_weather** - Mock weather data
|
||||
- **delay** - Delays execution for testing timeouts
|
||||
- **throw_error** - Throws an error for testing error handling
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build
|
||||
npm run build
|
||||
|
||||
# Run
|
||||
node dist/index.js
|
||||
```
|
||||
|
||||
## Integration Testing
|
||||
|
||||
This server is designed to be used with Bifrost's MCP integration tests via STDIO transport.
|
||||
1161
examples/mcps/test-tools-server/package-lock.json
generated
Normal file
1161
examples/mcps/test-tools-server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
examples/mcps/test-tools-server/package.json
Normal file
24
examples/mcps/test-tools-server/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "test-tools-server",
|
||||
"version": "1.0.0",
|
||||
"description": "MCP STDIO server with standard test tools for integration testing",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"test-tools-server": "./dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc && chmod +x dist/index.js",
|
||||
"prepare": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"overrides": {
|
||||
"hono": "4.12.14"
|
||||
}
|
||||
}
|
||||
270
examples/mcps/test-tools-server/src/index.ts
Normal file
270
examples/mcps/test-tools-server/src/index.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { z } from "zod";
|
||||
|
||||
// Tool input schemas
|
||||
const EchoSchema = z.object({
|
||||
message: z.string().describe("The message to echo back"),
|
||||
});
|
||||
|
||||
const CalculatorSchema = z.object({
|
||||
operation: z.enum(["add", "subtract", "multiply", "divide"]).describe("The operation to perform"),
|
||||
x: z.number().describe("First number"),
|
||||
y: z.number().describe("Second number"),
|
||||
});
|
||||
|
||||
const WeatherSchema = z.object({
|
||||
location: z.string().describe("The location to get weather for"),
|
||||
units: z.string().optional().describe("Temperature units (celsius or fahrenheit)"),
|
||||
});
|
||||
|
||||
const DelaySchema = z.object({
|
||||
seconds: z.number().describe("Number of seconds to delay"),
|
||||
});
|
||||
|
||||
const ThrowErrorSchema = z.object({
|
||||
error_message: z.string().describe("The error message to throw"),
|
||||
});
|
||||
|
||||
// Create the MCP server
|
||||
const server = new Server(
|
||||
{
|
||||
name: "test-tools-server",
|
||||
version: "1.0.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Handler for listing available tools
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: [
|
||||
{
|
||||
name: "echo",
|
||||
description: "Echoes back the provided message",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
message: {
|
||||
type: "string",
|
||||
description: "The message to echo back",
|
||||
},
|
||||
},
|
||||
required: ["message"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "calculator",
|
||||
description: "Performs basic arithmetic operations",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
operation: {
|
||||
type: "string",
|
||||
enum: ["add", "subtract", "multiply", "divide"],
|
||||
description: "The operation to perform",
|
||||
},
|
||||
x: {
|
||||
type: "number",
|
||||
description: "First number",
|
||||
},
|
||||
y: {
|
||||
type: "number",
|
||||
description: "Second number",
|
||||
},
|
||||
},
|
||||
required: ["operation", "x", "y"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_weather",
|
||||
description: "Gets weather information for a location",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
location: {
|
||||
type: "string",
|
||||
description: "The location to get weather for",
|
||||
},
|
||||
units: {
|
||||
type: "string",
|
||||
description: "Temperature units (celsius or fahrenheit)",
|
||||
},
|
||||
},
|
||||
required: ["location"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "delay",
|
||||
description: "Delays execution for specified seconds",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
seconds: {
|
||||
type: "number",
|
||||
description: "Number of seconds to delay",
|
||||
},
|
||||
},
|
||||
required: ["seconds"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "throw_error",
|
||||
description: "Throws an error with specified message",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
error_message: {
|
||||
type: "string",
|
||||
description: "The error message to throw",
|
||||
},
|
||||
},
|
||||
required: ["error_message"],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
// Handler for tool execution
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
try {
|
||||
const toolName = request.params.name;
|
||||
|
||||
switch (toolName) {
|
||||
case "echo": {
|
||||
const args = EchoSchema.parse(request.params.arguments);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({ message: args.message }),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "calculator": {
|
||||
const args = CalculatorSchema.parse(request.params.arguments);
|
||||
let result: number;
|
||||
|
||||
switch (args.operation) {
|
||||
case "add":
|
||||
result = args.x + args.y;
|
||||
break;
|
||||
case "subtract":
|
||||
result = args.x - args.y;
|
||||
break;
|
||||
case "multiply":
|
||||
result = args.x * args.y;
|
||||
break;
|
||||
case "divide":
|
||||
if (args.y === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Error: Division by zero",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
result = args.x / args.y;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({ result }),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "get_weather": {
|
||||
const args = WeatherSchema.parse(request.params.arguments);
|
||||
// Mock weather data
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
location: args.location,
|
||||
temperature: 72,
|
||||
units: args.units || "fahrenheit",
|
||||
condition: "Partly Cloudy",
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "delay": {
|
||||
const args = DelaySchema.parse(request.params.arguments);
|
||||
await new Promise((resolve) => setTimeout(resolve, args.seconds * 1000));
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
delayed_seconds: args.seconds,
|
||||
message: `Delayed for ${args.seconds} seconds`,
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "throw_error": {
|
||||
const args = ThrowErrorSchema.parse(request.params.arguments);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: args.error_message,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Start the server with stdio transport
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("Test Tools MCP Server running on stdio");
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Fatal error in main():", error);
|
||||
process.exit(1);
|
||||
});
|
||||
17
examples/mcps/test-tools-server/tsconfig.json
Normal file
17
examples/mcps/test-tools-server/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user