first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

View 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
)

View 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=

View 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
}

View 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"
}
}
```

View 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
)

View 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=

View 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
})
}

File diff suppressed because it is too large Load Diff

View 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"
}
}

View 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);
});

View 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"]
}

View 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
}
}
```

View 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
)

View 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=

View 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
})
}

File diff suppressed because it is too large Load Diff

View 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"
}
}

View 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);
});

View 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"]
}

View 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{},
},
}
```

View 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
)

View 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=

View 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
})
}

View 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)

View 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
)

View 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=

View 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)
})
}

View 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
)

View 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=

View 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
})
}

File diff suppressed because it is too large Load Diff

View 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"
}
}

View 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);
});

View 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"]
}

View 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

File diff suppressed because it is too large Load Diff

View 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"
}
}

View 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);
});

View 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"]
}

View 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.

File diff suppressed because it is too large Load Diff

View 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"
}
}

View 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);
});

View 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"]
}