first commit
This commit is contained in:
171
examples/mcps/http-no-ping-server/README.md
Normal file
171
examples/mcps/http-no-ping-server/README.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# HTTP MCP Server Without Ping Support
|
||||
|
||||
This is a sample MCP server implementation that runs over HTTP but **does not support the optional `ping` method**. This demonstrates how to configure Bifrost to use the `listTools` health check method instead of ping.
|
||||
|
||||
## What is This?
|
||||
|
||||
Many MCP servers may not implement the optional `ping` method from the MCP specification. This example shows:
|
||||
|
||||
1. **How to build an MCP server** that only supports the core methods (`list_tools`, `call_tool`) but not `ping`
|
||||
2. **How to configure Bifrost** to work with such servers using `is_ping_available: false`
|
||||
3. **Why this matters**: When `is_ping_available` is `false`, Bifrost will use `listTools` for health checks instead of the lightweight `ping` method
|
||||
|
||||
## Running the Server
|
||||
|
||||
### Prerequisites
|
||||
|
||||
```bash
|
||||
go 1.26.1+
|
||||
```
|
||||
|
||||
### Start the Server
|
||||
|
||||
```bash
|
||||
# From this directory
|
||||
go run main.go
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
MCP server listening on http://localhost:3001/mcp
|
||||
Note: This server does NOT support ping. Use is_ping_available=false in Bifrost config.
|
||||
```
|
||||
|
||||
## Connecting via Bifrost
|
||||
|
||||
### Configuration (config.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"client_configs": [
|
||||
{
|
||||
"name": "http_no_ping_server",
|
||||
"connection_type": "http",
|
||||
"connection_string": "http://localhost:3001/mcp",
|
||||
"is_ping_available": false,
|
||||
"tools_to_execute": ["*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Via API
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/mcp/client \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "http_no_ping_server",
|
||||
"connection_type": "http",
|
||||
"connection_string": "http://localhost:3001/mcp",
|
||||
"is_ping_available": false,
|
||||
"tools_to_execute": ["*"]
|
||||
}'
|
||||
```
|
||||
|
||||
### Via Web UI
|
||||
|
||||
1. Navigate to **MCP Gateway**
|
||||
2. Click **New MCP Server**
|
||||
3. Fill in:
|
||||
- **Name**: `http_no_ping_server`
|
||||
- **Connection Type**: HTTP
|
||||
- **Connection URL**: `http://localhost:3001/mcp`
|
||||
- **Ping Available for Health Check**: Toggle OFF (disabled)
|
||||
4. Click **Create**
|
||||
|
||||
## Available Tools
|
||||
|
||||
This server provides three simple tools for testing:
|
||||
|
||||
### 1. echo
|
||||
Echoes back the input message.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "echo",
|
||||
"arguments": {
|
||||
"message": "Hello, World!"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. add
|
||||
Adds two numbers together.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "add",
|
||||
"arguments": {
|
||||
"a": 5,
|
||||
"b": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. greet
|
||||
Greets someone by name.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "greet",
|
||||
"arguments": {
|
||||
"name": "Alice"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Health Check Behavior
|
||||
|
||||
When you add this server to Bifrost with `is_ping_available: false`:
|
||||
|
||||
1. Bifrost will **NOT** send `ping` requests (since the server doesn't support them)
|
||||
2. Instead, Bifrost will use `listTools` every 10 seconds to check server health
|
||||
3. If `listTools` fails 5 consecutive times, the server will be marked as `disconnected`
|
||||
|
||||
**Why `listTools` instead of `ping`?**
|
||||
- `ping` is lighter and faster, but optional in MCP
|
||||
- `listTools` is heavier but guaranteed to exist on all MCP servers
|
||||
- Using `listTools` for health checks is a fallback for servers without `ping` support
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
This example intentionally:
|
||||
|
||||
- ✅ Supports all core MCP methods (list_tools, call_tool)
|
||||
- ✅ Returns proper JSON-RPC responses
|
||||
- ✅ Works over HTTP
|
||||
- ❌ Does NOT implement the `ping` method
|
||||
- ❌ Returns a JSON-RPC method-not-found error (-32601) when ping is attempted
|
||||
|
||||
### How Ping is Blocked
|
||||
|
||||
The mcp-go library's `NewStreamableHTTPServer` automatically includes ping support by default. To demonstrate a server without ping, this example uses **HTTP middleware** that:
|
||||
|
||||
1. Intercepts all POST requests
|
||||
2. Checks if the request is a `ping` method call
|
||||
3. If it's a ping request, returns a JSON-RPC error: `{"code": -32601, "message": "Method not found: ping is not supported by this server"}`
|
||||
4. For all other requests (list_tools, call_tool), passes them through normally
|
||||
|
||||
This allows us to:
|
||||
- ✅ Keep the simple mcp-go server implementation
|
||||
- ✅ Transparently block ping requests at the HTTP layer
|
||||
- ✅ Return proper JSON-RPC error responses
|
||||
- ✅ Demonstrate the `is_ping_available=false` behavior in Bifrost
|
||||
|
||||
## Key Learning: is_ping_available
|
||||
|
||||
The `is_ping_available` setting is important because:
|
||||
|
||||
| Setting | Health Check Method | When to Use |
|
||||
|---------|-------------------|-----------|
|
||||
| `true` (default) | Lightweight `ping` | When your server supports ping (recommended) |
|
||||
| `false` | Heavier `listTools` | When your server doesn't support ping |
|
||||
|
||||
## See Also
|
||||
|
||||
- [MCP Specification](https://spec.modelcontextprotocol.io/)
|
||||
- [Bifrost MCP Documentation](../../docs/mcp/connecting-to-servers.mdx)
|
||||
- [Health Monitoring Guide](../../docs/mcp/connecting-to-servers.mdx#health-monitoring)
|
||||
17
examples/mcps/http-no-ping-server/go.mod
Normal file
17
examples/mcps/http-no-ping-server/go.mod
Normal file
@@ -0,0 +1,17 @@
|
||||
module http-no-ping-server
|
||||
|
||||
go 1.26.2
|
||||
|
||||
require github.com/mark3labs/mcp-go v0.43.2
|
||||
|
||||
require (
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
39
examples/mcps/http-no-ping-server/go.sum
Normal file
39
examples/mcps/http-no-ping-server/go.sum
Normal file
@@ -0,0 +1,39 @@
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
||||
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I=
|
||||
github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
194
examples/mcps/http-no-ping-server/main.go
Normal file
194
examples/mcps/http-no-ping-server/main.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
// NoPingMCPServer is an HTTP MCP server that intentionally does not support ping.
|
||||
// This demonstrates how to configure servers with is_ping_available=false in Bifrost
|
||||
// when your MCP server implementation doesn't support the optional ping method.
|
||||
func main() {
|
||||
// Create MCP server
|
||||
mcpServer := server.NewMCPServer(
|
||||
"http-no-ping-server",
|
||||
"1.0.0",
|
||||
)
|
||||
|
||||
// Define tools using the proper NewTool API
|
||||
echoTool := mcp.NewTool(
|
||||
"echo",
|
||||
mcp.WithDescription("Echo back the input message"),
|
||||
mcp.WithString("message", mcp.Required(), mcp.Description("Message to echo")),
|
||||
)
|
||||
|
||||
addTool := mcp.NewTool(
|
||||
"add",
|
||||
mcp.WithDescription("Add two numbers"),
|
||||
mcp.WithNumber("a", mcp.Required(), mcp.Description("First number")),
|
||||
mcp.WithNumber("b", mcp.Required(), mcp.Description("Second number")),
|
||||
)
|
||||
|
||||
greetTool := mcp.NewTool(
|
||||
"greet",
|
||||
mcp.WithDescription("Greet someone by name"),
|
||||
mcp.WithString("name", mcp.Required(), mcp.Description("Name to greet")),
|
||||
)
|
||||
|
||||
// Register tool handlers
|
||||
mcpServer.AddTool(echoTool, echoHandler)
|
||||
mcpServer.AddTool(addTool, addHandler)
|
||||
mcpServer.AddTool(greetTool, greetHandler)
|
||||
|
||||
// Create HTTP server using StreamableHTTP transport
|
||||
httpServer := server.NewStreamableHTTPServer(mcpServer)
|
||||
|
||||
port := 3001
|
||||
addr := fmt.Sprintf("localhost:%d", port)
|
||||
|
||||
log.Printf("MCP server listening on http://%s/", addr)
|
||||
log.Printf("Note: This server does NOT support ping. Use is_ping_available=false in Bifrost config.")
|
||||
log.Printf("\nExample Bifrost config:")
|
||||
log.Printf(`
|
||||
{
|
||||
"name": "http_no_ping_server",
|
||||
"connection_type": "http",
|
||||
"connection_string": "http://%s/",
|
||||
"is_ping_available": false,
|
||||
"tools_to_execute": ["*"]
|
||||
}
|
||||
`, addr)
|
||||
|
||||
// Wrap the HTTP server with middleware that rejects ping requests
|
||||
wrappedHandler := noPingMiddleware(httpServer)
|
||||
|
||||
if err := http.ListenAndServe(addr, wrappedHandler); err != nil {
|
||||
log.Fatalf("Server error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// echoHandler handles the echo tool
|
||||
func echoHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
// Extract arguments as JSON
|
||||
var args struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Parse the arguments
|
||||
argBytes, err := json.Marshal(request.Params.Arguments)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to parse arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(argBytes, &args); err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Invalid arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("Echo: %s", args.Message)
|
||||
return mcp.NewToolResultText(result), nil
|
||||
}
|
||||
|
||||
// addHandler handles the add tool
|
||||
func addHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
// Extract arguments as JSON
|
||||
var args struct {
|
||||
A float64 `json:"a"`
|
||||
B float64 `json:"b"`
|
||||
}
|
||||
|
||||
// Parse the arguments
|
||||
argBytes, err := json.Marshal(request.Params.Arguments)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to parse arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(argBytes, &args); err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Invalid arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
result := args.A + args.B
|
||||
return mcp.NewToolResultText(fmt.Sprintf("%v + %v = %v", args.A, args.B, result)), nil
|
||||
}
|
||||
|
||||
// greetHandler handles the greet tool
|
||||
func greetHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
// Extract arguments as JSON
|
||||
var args struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Parse the arguments
|
||||
argBytes, err := json.Marshal(request.Params.Arguments)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to parse arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(argBytes, &args); err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Invalid arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("Hello, %s! Welcome to the MCP server.", args.Name)
|
||||
return mcp.NewToolResultText(result), nil
|
||||
}
|
||||
|
||||
// noPingMiddleware is HTTP middleware that rejects ping requests
|
||||
// This allows us to demonstrate a server that doesn't support ping
|
||||
func noPingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Only intercept POST requests (MCP messages)
|
||||
if r.Method != http.MethodPost {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Read the request body
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to read request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the JSON-RPC request to check if it's a ping request
|
||||
var jsonRequest map[string]interface{}
|
||||
if err := json.Unmarshal(body, &jsonRequest); err != nil {
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this is a ping request
|
||||
if method, ok := jsonRequest["method"].(string); ok && method == "ping" {
|
||||
// Reject ping requests with a method not found error
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
var id interface{}
|
||||
if idVal, ok := jsonRequest["id"]; ok {
|
||||
id = idVal
|
||||
}
|
||||
|
||||
errorResponse := map[string]interface{}{
|
||||
"jsonrpc": "2.0",
|
||||
"error": map[string]interface{}{
|
||||
"code": -32601,
|
||||
"message": "Method not found: ping is not supported by this server",
|
||||
},
|
||||
"id": id,
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(errorResponse)
|
||||
return
|
||||
}
|
||||
|
||||
// For non-ping requests, restore the body and pass through to the next handler
|
||||
r.Body = io.NopCloser(strings.NewReader(string(body)))
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user