first commit
This commit is contained in:
70
examples/mcps/error-test-server/README.md
Normal file
70
examples/mcps/error-test-server/README.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Error Test MCP Server
|
||||
|
||||
MCP STDIO server optimized for testing error scenarios and edge cases.
|
||||
|
||||
## Tools
|
||||
|
||||
- **malformed_json** - Returns malformed JSON (truncated, invalid escapes, unclosed brackets, mixed types)
|
||||
- **timeout_tool** - Hangs for specified duration to test timeout handling
|
||||
- **intermittent_fail** - Randomly fails based on fail_rate to test retry logic
|
||||
- **network_error** - Simulates network errors (connection refused, timeout, DNS failure, SSL errors)
|
||||
- **large_payload** - Returns very large payloads to test size limits
|
||||
- **partial_response** - Returns incomplete responses to test handling
|
||||
- **invalid_content_type** - Returns content with mismatched type declaration
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build
|
||||
npm run build
|
||||
|
||||
# Run
|
||||
node dist/index.js
|
||||
```
|
||||
|
||||
## Integration Testing
|
||||
|
||||
This server is designed to test error handling in Bifrost's MCP integration via STDIO transport.
|
||||
|
||||
### Example Tool Calls
|
||||
|
||||
```typescript
|
||||
// Test malformed JSON
|
||||
{
|
||||
"name": "malformed_json",
|
||||
"arguments": {
|
||||
"id": "test-1",
|
||||
"json_type": "truncated"
|
||||
}
|
||||
}
|
||||
|
||||
// Test timeout
|
||||
{
|
||||
"name": "timeout_tool",
|
||||
"arguments": {
|
||||
"id": "test-2",
|
||||
"timeout_ms": 3000
|
||||
}
|
||||
}
|
||||
|
||||
// Test intermittent failures
|
||||
{
|
||||
"name": "intermittent_fail",
|
||||
"arguments": {
|
||||
"id": "test-3",
|
||||
"fail_rate": 0.7
|
||||
}
|
||||
}
|
||||
|
||||
// Test large payloads
|
||||
{
|
||||
"name": "large_payload",
|
||||
"arguments": {
|
||||
"id": "test-4",
|
||||
"size_kb": 500
|
||||
}
|
||||
}
|
||||
```
|
||||
17
examples/mcps/error-test-server/go.mod
Normal file
17
examples/mcps/error-test-server/go.mod
Normal file
@@ -0,0 +1,17 @@
|
||||
module github.com/maximhq/bifrost/examples/mcps/error-test-server
|
||||
|
||||
go 1.26.2
|
||||
|
||||
require github.com/mark3labs/mcp-go v0.43.2
|
||||
|
||||
require (
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
39
examples/mcps/error-test-server/go.sum
Normal file
39
examples/mcps/error-test-server/go.sum
Normal file
@@ -0,0 +1,39 @@
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
||||
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I=
|
||||
github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
279
examples/mcps/error-test-server/main.go
Normal file
279
examples/mcps/error-test-server/main.go
Normal file
@@ -0,0 +1,279 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Seed random number generator
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
// Create MCP server
|
||||
s := server.NewMCPServer(
|
||||
"error-test-server",
|
||||
"1.0.0",
|
||||
server.WithToolCapabilities(true),
|
||||
)
|
||||
|
||||
// Register all tools
|
||||
registerTimeoutAfterTool(s)
|
||||
registerReturnMalformedJSONTool(s)
|
||||
registerReturnErrorTool(s)
|
||||
registerIntermittentFailTool(s)
|
||||
registerMemoryIntensiveTool(s)
|
||||
|
||||
// Start STDIO server
|
||||
if err := server.ServeStdio(s); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Server error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 1: timeout_after
|
||||
// ============================================================================
|
||||
|
||||
func registerTimeoutAfterTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("timeout_after",
|
||||
mcp.WithDescription("Simulates a timeout by delaying for specified seconds"),
|
||||
mcp.WithNumber("seconds",
|
||||
mcp.Required(),
|
||||
mcp.Description("Number of seconds to wait before responding"),
|
||||
),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
var args struct {
|
||||
Seconds float64 `json:"seconds"`
|
||||
}
|
||||
|
||||
// Get arguments using the proper method
|
||||
argsInterface := request.GetArguments()
|
||||
|
||||
// Marshal and unmarshal to convert to our struct
|
||||
argsBytes, err := json.Marshal(argsInterface)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(argsBytes, &args); err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Invalid arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
duration := time.Duration(args.Seconds * float64(time.Second))
|
||||
|
||||
// Use context-aware sleep
|
||||
select {
|
||||
case <-time.After(duration):
|
||||
response := map[string]interface{}{
|
||||
"delayed_seconds": args.Seconds,
|
||||
"message": fmt.Sprintf("Delayed for %.2f seconds", args.Seconds),
|
||||
}
|
||||
jsonResult, _ := json.Marshal(response)
|
||||
return mcp.NewToolResultText(string(jsonResult)), nil
|
||||
case <-ctx.Done():
|
||||
return mcp.NewToolResultError("Operation cancelled or timed out"), nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 2: return_malformed_json
|
||||
// ============================================================================
|
||||
|
||||
func registerReturnMalformedJSONTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("return_malformed_json",
|
||||
mcp.WithDescription("Returns intentionally malformed JSON to test error handling"),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
// Return deliberately broken JSON
|
||||
// Note: This will be wrapped in the MCP protocol, so the MCP layer should handle it
|
||||
// But the content itself is invalid JSON
|
||||
malformedJSON := `{"key": "value", "broken": }`
|
||||
return mcp.NewToolResultText(malformedJSON), nil
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 3: return_error
|
||||
// ============================================================================
|
||||
|
||||
func registerReturnErrorTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("return_error",
|
||||
mcp.WithDescription("Returns an error with specified type"),
|
||||
mcp.WithString("error_type",
|
||||
mcp.Required(),
|
||||
mcp.Description("Type of error to return"),
|
||||
mcp.Enum("validation", "runtime", "network", "timeout", "permission"),
|
||||
),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
var args struct {
|
||||
ErrorType string `json:"error_type"`
|
||||
}
|
||||
|
||||
// Get arguments using the proper method
|
||||
argsInterface := request.GetArguments()
|
||||
|
||||
// Marshal and unmarshal to convert to our struct
|
||||
argsBytes, err := json.Marshal(argsInterface)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(argsBytes, &args); err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Invalid arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
var errorMessage string
|
||||
switch args.ErrorType {
|
||||
case "validation":
|
||||
errorMessage = "Validation Error: Invalid input parameters provided"
|
||||
case "runtime":
|
||||
errorMessage = "Runtime Error: Unexpected condition occurred during execution"
|
||||
case "network":
|
||||
errorMessage = "Network Error: Failed to connect to remote service"
|
||||
case "timeout":
|
||||
errorMessage = "Timeout Error: Operation exceeded maximum allowed time"
|
||||
case "permission":
|
||||
errorMessage = "Permission Error: Insufficient privileges to perform operation"
|
||||
default:
|
||||
errorMessage = fmt.Sprintf("Unknown error type: %s", args.ErrorType)
|
||||
}
|
||||
|
||||
return mcp.NewToolResultError(errorMessage), nil
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 4: intermittent_fail
|
||||
// ============================================================================
|
||||
|
||||
func registerIntermittentFailTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("intermittent_fail",
|
||||
mcp.WithDescription("Fails randomly based on specified fail rate percentage (0-100)"),
|
||||
mcp.WithNumber("fail_rate",
|
||||
mcp.Required(),
|
||||
mcp.Description("Percentage chance of failure (0-100)"),
|
||||
),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
var args struct {
|
||||
FailRate float64 `json:"fail_rate"`
|
||||
}
|
||||
|
||||
// Get arguments using the proper method
|
||||
argsInterface := request.GetArguments()
|
||||
|
||||
// Marshal and unmarshal to convert to our struct
|
||||
argsBytes, err := json.Marshal(argsInterface)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(argsBytes, &args); err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Invalid arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
// Validate fail rate
|
||||
if args.FailRate < 0 || args.FailRate > 100 {
|
||||
return mcp.NewToolResultError("Fail rate must be between 0 and 100"), nil
|
||||
}
|
||||
|
||||
// Generate random number between 0-100
|
||||
randomValue := rand.Float64() * 100
|
||||
|
||||
if randomValue < args.FailRate {
|
||||
// Fail
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Intermittent failure (fail_rate: %.1f%%, random: %.2f)", args.FailRate, randomValue)), nil
|
||||
}
|
||||
|
||||
// Success
|
||||
response := map[string]interface{}{
|
||||
"success": true,
|
||||
"fail_rate": args.FailRate,
|
||||
"random": randomValue,
|
||||
"message": "Operation succeeded",
|
||||
}
|
||||
|
||||
jsonResult, _ := json.Marshal(response)
|
||||
return mcp.NewToolResultText(string(jsonResult)), nil
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 5: memory_intensive
|
||||
// ============================================================================
|
||||
|
||||
func registerMemoryIntensiveTool(s *server.MCPServer) {
|
||||
tool := mcp.NewTool("memory_intensive",
|
||||
mcp.WithDescription("Allocates specified amount of memory to test resource limits"),
|
||||
mcp.WithNumber("size_mb",
|
||||
mcp.Required(),
|
||||
mcp.Description("Amount of memory to allocate in megabytes"),
|
||||
),
|
||||
)
|
||||
|
||||
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
var args struct {
|
||||
SizeMB int `json:"size_mb"`
|
||||
}
|
||||
|
||||
// Get arguments using the proper method
|
||||
argsInterface := request.GetArguments()
|
||||
|
||||
// Marshal and unmarshal to convert to our struct
|
||||
argsBytes, err := json.Marshal(argsInterface)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(argsBytes, &args); err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("Invalid arguments: %v", err)), nil
|
||||
}
|
||||
|
||||
// Limit to reasonable size to prevent crashes
|
||||
if args.SizeMB > 100 {
|
||||
return mcp.NewToolResultError("Size limited to 100MB for safety"), nil
|
||||
}
|
||||
|
||||
// Allocate memory (use int64 to prevent overflow)
|
||||
sizeBytes := int64(args.SizeMB) * 1024 * 1024
|
||||
data := make([]byte, sizeBytes)
|
||||
|
||||
// Fill with pattern to ensure allocation
|
||||
for i := range data {
|
||||
data[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
// Calculate checksum to verify allocation
|
||||
var checksum uint64
|
||||
for _, b := range data {
|
||||
checksum += uint64(b)
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"allocated_mb": args.SizeMB,
|
||||
"allocated_bytes": sizeBytes,
|
||||
"checksum": checksum,
|
||||
"message": fmt.Sprintf("Successfully allocated %dMB", args.SizeMB),
|
||||
}
|
||||
|
||||
// Clear memory before returning
|
||||
data = nil
|
||||
|
||||
jsonResult, _ := json.Marshal(response)
|
||||
return mcp.NewToolResultText(string(jsonResult)), nil
|
||||
})
|
||||
}
|
||||
1161
examples/mcps/error-test-server/package-lock.json
generated
Normal file
1161
examples/mcps/error-test-server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
examples/mcps/error-test-server/package.json
Normal file
24
examples/mcps/error-test-server/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "error-test-server",
|
||||
"version": "1.0.0",
|
||||
"description": "MCP STDIO server optimized for testing error scenarios and edge cases",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"error-test-server": "./dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc && chmod +x dist/index.js",
|
||||
"prepare": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"overrides": {
|
||||
"hono": "4.12.14"
|
||||
}
|
||||
}
|
||||
373
examples/mcps/error-test-server/src/index.ts
Normal file
373
examples/mcps/error-test-server/src/index.ts
Normal file
@@ -0,0 +1,373 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { z } from "zod";
|
||||
|
||||
// Schemas for error test tools
|
||||
const MalformedJsonSchema = z.object({
|
||||
id: z.string().describe("Tool invocation ID"),
|
||||
json_type: z.enum(["truncated", "invalid_escape", "unclosed_bracket", "mixed_types"]).optional(),
|
||||
});
|
||||
|
||||
const TimeoutToolSchema = z.object({
|
||||
id: z.string().describe("Tool invocation ID"),
|
||||
timeout_ms: z.number().optional().describe("Timeout duration in milliseconds (default 5000)"),
|
||||
});
|
||||
|
||||
const IntermittentFailSchema = z.object({
|
||||
id: z.string().describe("Tool invocation ID"),
|
||||
fail_rate: z.number().min(0).max(1).optional().describe("Probability of failure (0-1, default 0.5)"),
|
||||
});
|
||||
|
||||
const NetworkErrorSchema = z.object({
|
||||
id: z.string().describe("Tool invocation ID"),
|
||||
error_type: z.enum(["connection_refused", "timeout", "dns_failure", "ssl_error"]).optional(),
|
||||
});
|
||||
|
||||
const LargePayloadSchema = z.object({
|
||||
id: z.string().describe("Tool invocation ID"),
|
||||
size_kb: z.number().optional().describe("Payload size in KB (default 100)"),
|
||||
});
|
||||
|
||||
const PartialResponseSchema = z.object({
|
||||
id: z.string().describe("Tool invocation ID"),
|
||||
break_at: z.enum(["start", "middle", "end"]).optional().describe("Where to break the response"),
|
||||
});
|
||||
|
||||
const server = new Server(
|
||||
{ name: "error-test-server", version: "1.0.0" },
|
||||
{ capabilities: { tools: {} } }
|
||||
);
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [
|
||||
{
|
||||
name: "malformed_json",
|
||||
description: "Returns malformed JSON to test error handling",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
json_type: {
|
||||
type: "string",
|
||||
enum: ["truncated", "invalid_escape", "unclosed_bracket", "mixed_types"],
|
||||
description: "Type of JSON malformation",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "timeout_tool",
|
||||
description: "Hangs for a specified duration to test timeouts",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
timeout_ms: {
|
||||
type: "number",
|
||||
description: "Timeout duration in milliseconds (default 5000)",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "intermittent_fail",
|
||||
description: "Randomly fails to test retry logic",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
fail_rate: {
|
||||
type: "number",
|
||||
minimum: 0,
|
||||
maximum: 1,
|
||||
description: "Probability of failure (0-1, default 0.5)",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "network_error",
|
||||
description: "Simulates various network errors",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
error_type: {
|
||||
type: "string",
|
||||
enum: ["connection_refused", "timeout", "dns_failure", "ssl_error"],
|
||||
description: "Type of network error to simulate",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "large_payload",
|
||||
description: "Returns a very large payload to test size limits",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
size_kb: {
|
||||
type: "number",
|
||||
description: "Payload size in KB (default 100)",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "partial_response",
|
||||
description: "Returns incomplete response to test handling",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
break_at: {
|
||||
type: "string",
|
||||
enum: ["start", "middle", "end"],
|
||||
description: "Where to break the response",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid_content_type",
|
||||
description: "Returns content with mismatched type declaration",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const toolName = request.params.name;
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
switch (toolName) {
|
||||
case "malformed_json": {
|
||||
const args = MalformedJsonSchema.parse(request.params.arguments);
|
||||
const jsonType = args.json_type || "truncated";
|
||||
|
||||
let malformedText: string;
|
||||
switch (jsonType) {
|
||||
case "truncated":
|
||||
malformedText = '{"status": "success", "data": {"items": [1, 2, 3';
|
||||
break;
|
||||
case "invalid_escape":
|
||||
malformedText = '{"status": "success", "message": "Invalid \\x escape"}';
|
||||
break;
|
||||
case "unclosed_bracket":
|
||||
malformedText = '{"status": "success", "data": [1, 2, 3]';
|
||||
break;
|
||||
case "mixed_types":
|
||||
malformedText = '{"status": "success", "value": NaN, "other": undefined}';
|
||||
break;
|
||||
default:
|
||||
malformedText = '{"incomplete": true';
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: malformedText,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "timeout_tool": {
|
||||
const args = TimeoutToolSchema.parse(request.params.arguments);
|
||||
const timeoutMs = args.timeout_ms || 5000;
|
||||
|
||||
// Hang for the specified duration
|
||||
await new Promise((resolve) => setTimeout(resolve, timeoutMs));
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
tool: "timeout_tool",
|
||||
id: args.id,
|
||||
timeout_ms: timeoutMs,
|
||||
message: "This should have timed out",
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "intermittent_fail": {
|
||||
const args = IntermittentFailSchema.parse(request.params.arguments);
|
||||
const failRate = args.fail_rate ?? 0.5;
|
||||
|
||||
// Randomly fail based on fail_rate
|
||||
if (Math.random() < failRate) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
error: "Intermittent failure occurred",
|
||||
id: args.id,
|
||||
fail_rate: failRate,
|
||||
}),
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
tool: "intermittent_fail",
|
||||
id: args.id,
|
||||
success: true,
|
||||
fail_rate: failRate,
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "network_error": {
|
||||
const args = NetworkErrorSchema.parse(request.params.arguments);
|
||||
const errorType = args.error_type || "connection_refused";
|
||||
|
||||
const errorMessages = {
|
||||
connection_refused: "Connection refused: Unable to connect to remote server",
|
||||
timeout: "Request timeout: Server did not respond within timeout period",
|
||||
dns_failure: "DNS resolution failed: Unable to resolve hostname",
|
||||
ssl_error: "SSL handshake failed: Certificate verification error",
|
||||
};
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
error: errorMessages[errorType],
|
||||
error_type: errorType,
|
||||
id: args.id,
|
||||
}),
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
case "large_payload": {
|
||||
const args = LargePayloadSchema.parse(request.params.arguments);
|
||||
const sizeKb = args.size_kb || 100;
|
||||
|
||||
// Generate a large string (approximately sizeKb KB)
|
||||
const chunkSize = 1024; // 1 KB chunks
|
||||
const chunks: string[] = [];
|
||||
for (let i = 0; i < sizeKb; i++) {
|
||||
chunks.push("x".repeat(chunkSize));
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
tool: "large_payload",
|
||||
id: args.id,
|
||||
size_kb: sizeKb,
|
||||
payload: chunks.join(""),
|
||||
message: `Generated ${sizeKb}KB payload`,
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "partial_response": {
|
||||
const args = PartialResponseSchema.parse(request.params.arguments);
|
||||
const breakAt = args.break_at || "middle";
|
||||
|
||||
let response: string;
|
||||
switch (breakAt) {
|
||||
case "start":
|
||||
response = '{"sta';
|
||||
break;
|
||||
case "middle":
|
||||
response = '{"status": "success", "data": {"incomplete';
|
||||
break;
|
||||
case "end":
|
||||
response = '{"status": "success", "data": {"complete": true}, "message": "Almost done"';
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: response,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "invalid_content_type": {
|
||||
const args = z.object({ id: z.string() }).parse(request.params.arguments);
|
||||
|
||||
// Return a response that claims to be JSON but isn't properly formatted
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "This is not valid JSON content but the server says it is",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("Error Test MCP Server running on stdio");
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Fatal error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
17
examples/mcps/error-test-server/tsconfig.json
Normal file
17
examples/mcps/error-test-server/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user