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,85 @@
services:
weaviate:
image: cr.weaviate.io/semitechnologies/weaviate:1.32.4
command:
- --host
- 0.0.0.0
- --port
- '8080'
- --scheme
- http
environment:
- CLUSTER_HOSTNAME=weaviate
- CLUSTER_ADVERTISE_ADDR=172.38.0.12
- CLUSTER_GOSSIP_BIND_PORT=7946
- CLUSTER_DATA_BIND_PORT=7947
- DISABLE_TELEMETRY=true
- PERSISTENCE_DATA_PATH=/var/lib/weaviate
- DEFAULT_VECTORIZER_MODULE=none
- ENABLE_MODULES=
- AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED=true
- LOG_LEVEL=info
ports:
- "9000:8080"
volumes:
- weaviate_data:/var/lib/weaviate
networks:
bifrost_network:
ipv4_address: 172.38.0.12
# Redis Stack instance for vector store tests
redis-stack:
image: redis/redis-stack:7.4.0-v6
command: redis-stack-server --protected-mode no
ports:
- "6379:6379"
- "8001:8001" # RedisInsight web UI
volumes:
- redis_data:/data
networks:
bifrost_network:
ipv4_address: 172.38.0.13
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 3
# Qdrant instance for vector store tests
qdrant:
image: qdrant/qdrant:v1.16.0
ports:
- "6334:6334" # gRPC API
volumes:
- qdrant_data:/qdrant/storage
networks:
bifrost_network:
ipv4_address: 172.38.0.14
# mitmproxy - HTTP proxy with Web UI for debugging
# Web UI: http://localhost:8081
# Proxy: http://localhost:8082
mitmproxy:
image: mitmproxy/mitmproxy:latest
container_name: bifrost-mitmproxy
command: mitmweb --web-host 0.0.0.0 --web-port 8081 --listen-host 0.0.0.0 --listen-port 8082 --set proxyauth=bifrost:secret-token --set web_password=secret-token
ports:
- "8082:8082" # Proxy port
- "8081:8081" # Web UI
networks:
bifrost_network:
ipv4_address: 172.38.0.16
networks:
bifrost_network:
driver: bridge
ipam:
config:
- subnet: 172.38.0.0/16
gateway: 172.38.0.1
volumes:
weaviate_data:
redis_data:
qdrant_data:

View File

@@ -0,0 +1,18 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"encryption_key": "<REPLACE_WITH_BASE64_KEY>",
"config_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "../../examples/configs/encryptionmigration/config.db"
}
},
"logs_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "../../examples/configs/encryptionmigration/logs.db"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"config_store": {
"enabled": false
},
"logs_store": {
"enabled": false
}
}

View File

@@ -0,0 +1,55 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"config_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "../../tests/configs/partial/config.db"
}
},
"logs_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "../../tests/configs/partial/logs.db"
}
},
"providers": {
"openai": {
"keys": [
{
"name": "openai-key-1",
"value": "sk-123",
"weight": 1,
"models": ["*"]
}
]
},
"anthropic": {
"keys": [
{
"name": "anthropic-key-1",
"value": "sk-456",
"weight": 1,
"models": ["*"]
}
]
},
"bedrock": {
"keys": [
{
"name": "bedrock-key-1",
"value": "ak-123",
"weight": 1,
"models": ["*"]
},
{
"name": "bedrock-key-2",
"value": "ak-456",
"weight": 1,
"models": ["*"]
}
]
}
}
}

View File

@@ -0,0 +1,116 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"version": 1,
"providers": {
"openai": {
"keys": [
{
"name": "main",
"value": "env.OPENAI_API_KEY",
"models": [],
"weight": 1.0
}
],
"network_config": {
"default_request_timeout_in_seconds": 30,
"max_retries": 2
}
},
"anthropic": {
"keys": [
{
"name": "primary",
"value": "env.ANTHROPIC_API_KEY",
"models": [],
"weight": 1.0
},
{
"name": "secondary",
"value": "env.ANTHROPIC_API_KEY_2",
"models": [],
"weight": 0.5
}
],
"network_config": {
"default_request_timeout_in_seconds": 60,
"max_retries": 1
}
}
},
"mcp": {
"client_configs": [
{
"name": "internal_tools",
"connection_type": "http",
"connection_string": "http://localhost:3001",
"auth_type": "none",
"tools_to_execute": ["*"],
"allow_on_all_virtual_keys": false
}
]
},
"config_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "../../examples/configs/v1compat/config.db"
}
},
"logs_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "../../examples/configs/v1compat/logs.db"
}
},
"governance": {
"virtual_keys": [
{
"id": "vk-full-access",
"name": "Full Access",
"description": "v1 compat: empty provider_configs and mcp_configs mean allow all",
"provider_configs": [],
"mcp_configs": []
},
{
"id": "vk-openai-restricted",
"name": "OpenAI Restricted",
"description": "v1 compat: explicit provider entry but empty allowed_models and key_ids mean allow all",
"provider_configs": [
{
"provider": "openai",
"allowed_models": [],
"key_ids": [],
"weight": 1.0
}
],
"mcp_configs": [
{
"mcp_client_name": "internal_tools",
"tools_to_execute": ["*"]
}
]
},
{
"id": "vk-anthropic-restricted",
"name": "Anthropic Restricted",
"description": "v1 compat: mix — one provider with specific model list, another with empty (allow all)",
"provider_configs": [
{
"provider": "anthropic",
"allowed_models": [],
"key_ids": [],
"weight": 1.0
}
],
"mcp_configs": []
}
]
}
}

View File

@@ -0,0 +1,25 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"governance": {
"auth_config": {
"admin_username": "env.BIFROST_ADMIN_USERNAME",
"admin_password": "env.BIFROST_ADMIN_PASSWORD",
"is_enabled": true,
"disable_auth_on_inference": false
}
},
"config_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "../../examples/configs/withauth/config.db"
}
},
"logs_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "../../examples/configs/withauth/logs.db"
}
}
}

View File

@@ -0,0 +1,39 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"config_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "./config.db"
}
},
"client": {
"compat": {
"convert_text_to_chat": true,
"convert_chat_to_responses": true,
"should_drop_params": true,
"should_convert_params": false
},
"enable_logging": true,
"initial_pool_size": 300,
"log_retention_days": 365,
"header_filter_config": {
"allowlist": ["x-bf-eh-user-id", "x-bf-eh-team-id"],
"denylist": []
},
"required_headers": ["x-bf-user-id"],
"logging_headers": ["x-bf-user-id", "x-bf-team-id"]
},
"providers": {
"openai": {
"keys": [
{
"name": "openai-key-1",
"value": "sk-dummy",
"weight": 1,
"models": ["*"]
}
]
}
}
}

View File

@@ -0,0 +1,58 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"config_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "../../examples/configs/withconfigstore/config.db"
}
},
"auth_config": {
"admin_username": "env.BIFROST_ADMIN_USERNAME",
"admin_password": "env.BIFROST_ADMIN_PASSWORD",
"is_enabled": true,
"disable_auth_on_inference": true
},
"governance": {
"virtual_keys": [
{
"id": "vk-gpt",
"value": "vk-gpt",
"is_active": true,
"provider_configs": [
{
"provider": "azure",
"key_ids": [
"*"
],
"allowed_models": [
"gpt-4.1-2025-04-14",
"gpt-4.1-mini-2025-04-14",
"gpt-4.1-nano-2025-04-14",
"gpt-4o-2024-08-06",
"gpt-4o-2024-11-20",
"gpt-4o-mini-2024-07-18",
"gpt-5-2025-08-07",
"gpt-5-mini-2025-08-07",
"gpt-5-nano-2025-08-07",
"gpt-5-pro-2025-10-06",
"gpt-5.1-2025-11-13",
"o1-2024-12-17",
"o3-2025-04-16",
"o3-mini-2025-01-31",
"o3-pro-2025-06-10",
"o4-mini-2025-04-16",
"text-embedding-3-large-1",
"text-embedding-3-small-1",
"text-embedding-ada-002",
"tts-hd-001",
"whisper-001"
],
"weight": 0.5
}
],
"name": "vk-gpt"
}
]
}
}

View File

@@ -0,0 +1,27 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"config_store": {
"enabled": true,
"type": "postgres",
"config": {
"host": "localhost",
"port": "5432",
"user": "bifrost",
"password": "bifrost_password",
"db_name": "bifrost",
"ssl_mode": "disable"
}
},
"logs_store": {
"enabled": true,
"type": "postgres",
"config": {
"host": "localhost",
"port": "5432",
"user": "bifrost",
"password": "bifrost_password",
"db_name": "bifrost",
"ssl_mode": "disable"
}
}
}

View File

@@ -0,0 +1,17 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"config_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "../../tests/configs/withconfigstorelogsstore/config.db"
}
},
"logs_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "../../tests/configs/withconfigstorelogsstore/logs.db"
}
}
}

View File

@@ -0,0 +1,18 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"config_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "../../examples/configs/withconfigstore/config.db"
}
},
"plugins": [
{
"enabled": true,
"name": "hello-world",
"path": "/Users/akshay/Codebase/universe/bifrost/examples/plugins/hello-world/build/hello-world.so",
"version": 3
}
]
}

View File

@@ -0,0 +1,29 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"version": 2,
"config_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "./config.db"
}
},
"framework": {
"pricing": {
"pricing_url": "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json",
"pricing_sync_interval": 86400
}
},
"providers": {
"openai": {
"keys": [
{
"name": "openai-key-1",
"value": "sk-dummy",
"weight": 1,
"models": ["*"]
}
]
}
}
}

View File

@@ -0,0 +1,37 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"config_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "./config.db"
}
},
"logs_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "./logs.db"
}
},
"large_payload_optimization": {
"enabled": true,
"request_threshold_bytes": 10485760,
"response_threshold_bytes": 10485760,
"prefetch_size_bytes": 65536,
"max_payload_bytes": 524288000,
"truncated_log_bytes": 1048576
},
"providers": {
"openai": {
"keys": [
{
"name": "openai-key-1",
"value": "sk-dummy",
"weight": 1,
"models": ["*"]
}
]
}
}
}

View File

@@ -0,0 +1,25 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"config_store": {
"enabled": false
},
"logs_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "../../tests/configs/withlogstore/logs.db"
}
},
"providers": {
"openai": {
"keys": [
{
"name": "openai-key-1",
"value": "sk-proj-abc",
"weight": 1,
"models": ["*"]
}
]
}
}
}

View File

@@ -0,0 +1,77 @@
# Bifrost behind NGINX (Docker Compose)
This example runs 3 Bifrost containers behind an NGINX reverse proxy.
## Files
- `docker-compose.yml` - Starts NGINX and 3 Bifrost nodes
- `nginx.conf` - Reverse proxy and load balancing config
- `config.json` - Shared Bifrost config for all nodes
- `.env.example` - Required environment variables
- `helm-values.yaml` - Helm values for Kubernetes + NGINX Ingress
- `k8s-ingress.yaml` - Standalone ingress manifest (non-Helm or override)
## Run
```bash
cd examples/configs/withnginxreverseproxy
cp .env.example .env
# Edit .env and set real values
docker compose config
docker compose up -d
docker compose ps
```
NGINX exposes Bifrost on `http://localhost:8080`.
## Verify
```bash
# Health through NGINX
curl -i http://localhost:8080/health
# Chat completion through NGINX
curl -sS http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "Say hello"}]
}'
```
Streaming check:
```bash
curl -N http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o-mini",
"stream": true,
"messages": [{"role": "user", "content": "stream test"}]
}'
```
## Stop
```bash
docker compose down
```
## Kubernetes / Helm
```bash
# Render manifests and verify ingress is present
helm template bifrost ./helm-charts/bifrost \
-f examples/configs/withnginxreverseproxy/helm-values.yaml
# Install (or upgrade) with this example
helm upgrade --install bifrost ./helm-charts/bifrost \
-f examples/configs/withnginxreverseproxy/helm-values.yaml
```
Validate ingress manifest only:
```bash
kubectl apply --dry-run=client -f examples/configs/withnginxreverseproxy/k8s-ingress.yaml
```

View File

@@ -0,0 +1,33 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"encryption_key": "env.BIFROST_ENCRYPTION_KEY",
"config_store": {
"enabled": false
},
"logs_store": {
"enabled": false
},
"client": {
"drop_excess_requests": false,
"enable_logging": true,
"allowed_origins": [
"*"
],
"max_request_body_size_mb": 100
},
"providers": {
"openai": {
"keys": [
{
"name": "openai-primary",
"value": "env.OPENAI_API_KEY",
"models": [
"gpt-4o-mini",
"gpt-4o"
],
"weight": 1
}
]
}
}
}

View File

@@ -0,0 +1,54 @@
services:
nginx:
image: nginx:alpine
container_name: bifrost-nginx
ports:
- "8080:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- bifrost-1
- bifrost-2
- bifrost-3
restart: unless-stopped
bifrost-1:
image: maximhq/bifrost:latest
container_name: bifrost-1
env_file:
- .env
volumes:
- ./config.json:/app/config.json:ro
- bifrost_data_1:/app/data
expose:
- "8080"
restart: unless-stopped
bifrost-2:
image: maximhq/bifrost:latest
container_name: bifrost-2
env_file:
- .env
volumes:
- ./config.json:/app/config.json:ro
- bifrost_data_2:/app/data
expose:
- "8080"
restart: unless-stopped
bifrost-3:
image: maximhq/bifrost:latest
container_name: bifrost-3
env_file:
- .env
volumes:
- ./config.json:/app/config.json:ro
- bifrost_data_3:/app/data
expose:
- "8080"
restart: unless-stopped
volumes:
bifrost_data_1:
bifrost_data_2:
bifrost_data_3:

View File

@@ -0,0 +1,43 @@
image:
tag: "v1.4.18"
replicaCount: 3
ingress:
enabled: true
className: nginx
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
nginx.ingress.kubernetes.io/proxy-buffering: "off"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
hosts:
- host: bifrost.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: bifrost-tls
hosts:
- bifrost.example.com
bifrost:
encryptionKey: "env.BIFROST_ENCRYPTION_KEY"
client:
enableLogging: true
maxRequestBodySizeMb: 100
providers:
openai:
keys:
- name: "openai-primary"
value: "env.OPENAI_API_KEY"
models: ["gpt-4o-mini", "gpt-4o"]
weight: 1
env:
- name: OPENAI_API_KEY
value: "replace-with-real-key"
- name: BIFROST_ENCRYPTION_KEY
value: "replace-with-32-byte-random-string"

View File

@@ -0,0 +1,27 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: bifrost
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
nginx.ingress.kubernetes.io/proxy-buffering: "off"
spec:
ingressClassName: nginx
rules:
- host: bifrost.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: bifrost
port:
number: 8080
tls:
- secretName: bifrost-tls
hosts:
- bifrost.example.com

View File

@@ -0,0 +1,35 @@
events {
worker_connections 1024;
}
http {
upstream bifrost_backend {
least_conn;
server bifrost-1:8080;
server bifrost-2:8080;
server bifrost-3:8080;
}
server {
listen 80;
server_name _;
location / {
proxy_pass http://bifrost_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_buffering off;
proxy_request_buffering off;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
}

View File

@@ -0,0 +1,16 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"logs_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "../../examples/configs/withobjectstoragegcs/logs.db"
},
"object_storage": {
"type": "gcs",
"bucket": "env.GCS_BUCKET",
"credentials": "env.GCS_KEY",
"prefix":"bifrost/logs"
}
}
}

View File

@@ -0,0 +1,18 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"logs_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "../../examples/configs/withobjectstorage/logs.db"
},
"object_storage": {
"type": "s3",
"bucket": "env.AWS_S3_BUCKET",
"region": "env.AWS_REGION",
"access_key_id": "env.AWS_ACCESS_KEY_ID",
"secret_access_key": "env.AWS_SECRET_ACCESS_KEY",
"prefix": "logs"
}
}
}

View File

@@ -0,0 +1,37 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"config_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "../../tests/configs/withobservability/config.db"
}
},
"logs_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "../../tests/configs/withobservability/logs.db"
}
},
"plugins": [
{
"enabled": true,
"name": "maxim",
"config": {
"api_key": "",
"log_repo_id": ""
}
},
{
"enabled": true,
"name": "otel",
"config": {
"service_name": "bifrost",
"collector_url": "http://localhost:4318/v1/traces",
"trace_type": "genai_extension",
"protocol": "http"
}
}
]
}

View File

@@ -0,0 +1,48 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"config_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "./config.db"
}
},
"logs_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "./logs.db"
}
},
"providers": {
"openai": {
"keys": [
{
"name": "openai-key-1",
"value": "sk-dummy",
"weight": 1,
"models": ["*"]
}
]
}
},
"plugins": [
{
"enabled": true,
"name": "otel",
"config": {
"service_name": "bifrost",
"collector_url": "http://localhost:4318/v1/traces",
"trace_type": "otel",
"protocol": "http",
"metrics_enabled": true,
"metrics_endpoint": "http://localhost:4318/v1/metrics",
"metrics_push_interval": 15,
"headers": {
"x-honeycomb-team": "env.HONEYCOMB_API_KEY"
},
"insecure": false
}
}
]
}

View File

@@ -0,0 +1,150 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"client": {
"allow_direct_keys": false,
"allowed_origins": [
"*"
],
"disable_content_logging": false,
"drop_excess_requests": false,
"enable_logging": true,
"enforce_auth_on_inference": true,
"initial_pool_size": 300,
"log_retention_days": 365,
"max_request_body_size_mb": 100
},
"config_store": {
"enabled": true,
"type": "postgres",
"config": {
"host": "localhost",
"port": "5432",
"user": "bifrost",
"password": "bifrost_password",
"db_name": "bifrost",
"ssl_mode": "disable"
}
},
"logs_store": {
"enabled": true,
"type": "postgres",
"config": {
"host": "localhost",
"port": "5432",
"user": "bifrost",
"password": "bifrost_password",
"db_name": "bifrost",
"ssl_mode": "disable"
}
},
"mcp": {
"client_configs": [
{
"name": "WeatherService",
"connection_type": "http",
"client_id": "weather-mcp-server",
"connection_string": "http://localhost:8080/mcp"
},
{
"name": "CalendarService",
"connection_type": "http",
"client_id": "calendar-mcp-server",
"connection_string": "http://localhost:8081/mcp"
}
]
},
"governance": {
"auth_config": {
"admin_password": "env.BIFROST_ADMIN_PASSWORD",
"admin_username": "env.BIFROST_ADMIN_USERNAME",
"disable_auth_on_inference": true,
"is_enabled": false
},
"virtual_keys": [
{
"id": "vk-ai-portal-prod",
"is_active": true,
"name": "ai-portal-production-key",
"description": "Virtual key for AI portal with MCP access to weather and calendar services",
"value": "env.BIFROST_VK_AI_PORTAL",
"mcp_configs": [
{
"mcp_client_name": "WeatherService",
"tools_to_execute": [
"*"
]
},
{
"mcp_client_name": "CalendarService",
"tools_to_execute": [
"get_events",
"create_event"
]
}
],
"provider_configs": [
{
"provider": "openai",
"weight": 1.0,
"allowed_models": [
"*"
],
"key_ids": [
"*"
]
}
]
},
{
"id": "vk-internal-tools",
"is_active": true,
"name": "internal-tools-key",
"description": "Virtual key for internal tools with limited MCP access",
"value": "env.BIFROST_VK_INTERNAL",
"mcp_configs": [
{
"mcp_client_name": "WeatherService",
"tools_to_execute": [
"get_current_weather"
]
}
],
"provider_configs": [
{
"provider": "openai",
"weight": 1.0,
"allowed_models": [
"*"
],
"key_ids": [
"*"
]
}
]
}
]
},
"plugins": [
{
"config": {
"is_vk_mandatory": true
},
"enabled": true,
"name": "governance"
}
],
"providers": {
"openai": {
"keys": [
{
"name": "openai-primary",
"value": "env.OPENAI_API_KEY",
"weight": 1,
"models": [
"*"
]
}
]
}
}
}

View File

@@ -0,0 +1,74 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"config_store": {
"enabled": false
},
"logs_store": {
"enabled": false
},
"governance": {
"pricing_overrides": [
{
"id": "override-global-gpt4o",
"name": "Global GPT-4o Pricing",
"scope_kind": "global",
"match_type": "exact",
"pattern": "gpt-4o",
"request_types": ["chat_completion"],
"pricing_patch": "{\"input_cost_per_token\":0.0000025,\"output_cost_per_token\":0.00001}"
},
{
"id": "override-global-claude-wildcard",
"name": "Global Claude Models Pricing",
"scope_kind": "global",
"match_type": "wildcard",
"pattern": "claude-*",
"request_types": ["chat_completion"],
"pricing_patch": "{\"input_cost_per_token\":0.000003,\"output_cost_per_token\":0.000015}"
},
{
"id": "override-provider-openai-gpt4o-mini",
"name": "OpenAI GPT-4o Mini Pricing",
"scope_kind": "provider",
"provider_id": "openai",
"match_type": "exact",
"pattern": "gpt-4o-mini",
"request_types": ["chat_completion"],
"pricing_patch": "{\"input_cost_per_token\":0.00000015,\"output_cost_per_token\":0.0000006}"
}
]
},
"plugins": [
{
"name": "governance",
"enabled": true,
"config": {
"is_vk_mandatory": false
}
}
],
"providers": {
"openai": {
"keys": [
{
"id": "key-openai-1",
"name": "openai-key-1",
"value": "env.OPENAI_API_KEY",
"weight": 1,
"models": ["*"]
}
]
},
"anthropic": {
"keys": [
{
"id": "key-anthropic-1",
"name": "anthropic-key-1",
"value": "env.ANTHROPIC_API_KEY",
"weight": 1,
"models": ["*"]
}
]
}
}
}

View File

@@ -0,0 +1,82 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"config_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "config.db"
}
},
"logs_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "logs.db"
}
},
"governance": {
"pricing_overrides": [
{
"id": "override-global-gpt4o",
"name": "Global GPT-4o Pricing",
"scope_kind": "global",
"match_type": "exact",
"pattern": "gpt-4o",
"request_types": ["chat_completion"],
"pricing_patch": "{\"input_cost_per_token\":0.0000025,\"output_cost_per_token\":0.00001}"
},
{
"id": "override-global-claude-wildcard",
"name": "Global Claude Models Pricing",
"scope_kind": "global",
"match_type": "wildcard",
"pattern": "claude-*",
"request_types": ["chat_completion"],
"pricing_patch": "{\"input_cost_per_token\":0.000003,\"output_cost_per_token\":0.000015}"
},
{
"id": "override-provider-openai-gpt4o-mini",
"name": "OpenAI GPT-4o Mini Pricing",
"scope_kind": "provider",
"provider_id": "openai",
"match_type": "exact",
"pattern": "gpt-4o-mini",
"request_types": ["chat_completion"],
"pricing_patch": "{\"input_cost_per_token\":0.00000015,\"output_cost_per_token\":0.0000006}"
}
]
},
"plugins": [
{
"name": "governance",
"enabled": true,
"config": {
"is_vk_mandatory": false
}
}
],
"providers": {
"openai": {
"keys": [
{
"id": "key-openai-1",
"name": "openai-key-1",
"value": "env.OPENAI_API_KEY",
"weight": 1,
"models": ["*"]
}
]
},
"anthropic": {
"keys": [
{
"id": "key-anthropic-1",
"name": "anthropic-key-1",
"value": "env.ANTHROPIC_API_KEY",
"weight": 1,
"models": ["*"]
}
]
}
}
}

View File

@@ -0,0 +1,245 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"providers": {
"openai": {
"keys": [
{
"name": "OpenAI API Key",
"value": "env.OPENAI_API_KEY",
"weight": 1,
"use_for_batch_api": true,
"models": [
"*"
]
}
],
"network_config": {
"default_request_timeout_in_seconds": 300
}
},
"anthropic": {
"keys": [
{
"name": "Anthropic API Key",
"value": "env.ANTHROPIC_API_KEY",
"weight": 1,
"use_for_batch_api": true,
"models": [
"*"
]
}
],
"network_config": {
"default_request_timeout_in_seconds": 300
}
},
"gemini": {
"keys": [
{
"value": "env.GEMINI_API_KEY",
"weight": 1,
"use_for_batch_api": true,
"models": [
"*"
],
"name": "gemini-key-1"
}
],
"network_config": {
"default_request_timeout_in_seconds": 300
}
},
"vertex": {
"keys": [
{
"name": "Vertex API Key",
"vertex_key_config": {
"project_id": "env.VERTEX_PROJECT_ID",
"region": "env.GOOGLE_LOCATION",
"auth_credentials": "env.VERTEX_CREDENTIALS"
},
"weight": 1,
"models": [
"*"
]
}
],
"network_config": {
"default_request_timeout_in_seconds": 300
}
},
"mistral": {
"keys": [
{
"name": "Mistral API Key",
"value": "env.MISTRAL_API_KEY",
"weight": 1,
"models": [
"*"
]
}
],
"network_config": {
"default_request_timeout_in_seconds": 300
}
},
"cohere": {
"keys": [
{
"name": "Cohere API Key",
"value": "env.COHERE_API_KEY",
"weight": 1,
"models": [
"*"
]
}
],
"network_config": {
"default_request_timeout_in_seconds": 300
}
},
"groq": {
"keys": [
{
"name": "Groq API Key",
"value": "env.GROQ_API_KEY",
"weight": 1,
"models": [
"*"
]
}
],
"network_config": {
"default_request_timeout_in_seconds": 300
}
},
"perplexity": {
"keys": [
{
"name": "Perplexity API Key",
"value": "env.PERPLEXITY_API_KEY",
"weight": 1,
"models": [
"*"
]
}
],
"network_config": {
"default_request_timeout_in_seconds": 300
}
},
"cerebras": {
"keys": [
{
"name": "Cerebras API Key",
"value": "env.CEREBRAS_API_KEY",
"weight": 1,
"models": [
"*"
]
}
],
"network_config": {
"default_request_timeout_in_seconds": 300
}
},
"openrouter": {
"keys": [
{
"name": "OpenRouter API Key",
"value": "env.OPENROUTER_API_KEY",
"weight": 1,
"models": [
"*"
]
}
],
"network_config": {
"default_request_timeout_in_seconds": 300
}
},
"azure": {
"keys": [
{
"name": "Azure API Key",
"value": "env.AZURE_API_KEY",
"azure_key_config": {
"endpoint": "env.AZURE_ENDPOINT",
"api_version": "env.AZURE_API_VERSION"
},
"weight": 1,
"models": [
"*"
]
}
],
"network_config": {
"default_request_timeout_in_seconds": 300
}
},
"bedrock": {
"keys": [
{
"name": "Bedrock API Key",
"bedrock_key_config": {
"access_key": "env.AWS_ACCESS_KEY_ID",
"secret_key": "env.AWS_SECRET_ACCESS_KEY",
"region": "env.AWS_REGION",
"arn": "env.AWS_ARN"
},
"weight": 1,
"use_for_batch_api": true,
"models": [
"*"
]
}
],
"network_config": {
"default_request_timeout_in_seconds": 300
}
}
},
"client": {
"drop_excess_requests": false,
"initial_pool_size": 300,
"allowed_origins": [
"*"
],
"enable_logging": true,
"enforce_auth_on_inference": false,
"allow_direct_keys": false,
"max_request_body_size_mb": 100
},
"config_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "../../examples/configs/withprompushgateway/config.db"
}
},
"logs_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "../../examples/configs/withprompushgateway/logs.db"
}
},
"plugins": [
{
"name": "telemetry",
"enabled": true,
"config": {
"push_gateway": {
"enabled": true,
"push_gateway_url": "http://localhost:9091",
"job_name": "bifrost",
"push_interval": 15,
"basic_auth": {
"username": "admin",
"password": "admin"
}
}
}
}
]
}

View File

@@ -0,0 +1,68 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"config_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "./config.db"
}
},
"governance": {
"routing_rules": [
{
"id": "rule-gpt4-to-azure",
"name": "Route GPT-4 to Azure",
"description": "Route all GPT-4 model requests to Azure provider",
"enabled": true,
"cel_expression": "model.startsWith('gpt-4')",
"targets": [
{ "provider": "azure", "weight": 1.0 }
],
"fallbacks": ["openai"],
"scope": "global",
"priority": 0
},
{
"id": "rule-high-budget-fallback",
"name": "Use fallback when budget high",
"description": "Route to fallback providers when budget usage exceeds 80%",
"enabled": true,
"cel_expression": "budget_used >= 80.0",
"targets": [
{ "provider": "anthropic", "weight": 0.5 },
{ "provider": "openai", "weight": 0.5 }
],
"fallbacks": ["anthropic", "openai"],
"scope": "global",
"priority": 10
},
{
"id": "rule-embedding-lightweight",
"name": "Use lightweight embeddings",
"description": "Route embedding requests to cost-effective provider",
"enabled": true,
"cel_expression": "request_type == 'embedding'",
"targets": [
{ "provider": "openai", "model": "text-embedding-3-small", "weight": 1.0 }
],
"fallbacks": [],
"scope": "global",
"priority": 5
},
{
"id": "rule-premium-vk-azure",
"name": "Premium VK gets Azure",
"description": "Premium virtual keys always use Azure",
"enabled": true,
"cel_expression": "virtual_key_name == 'premium-tier'",
"targets": [
{ "provider": "azure", "weight": 1.0 }
],
"fallbacks": ["openai", "anthropic"],
"scope": "virtual_key",
"scope_id": "premium-tier",
"priority": 0
}
]
}
}

View File

@@ -0,0 +1,22 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"vector_store": {
"enabled": true,
"type": "weaviate",
"config": {
"scheme": "http",
"host": "localhost:9000"
}
},
"plugins": [
{
"enabled": true,
"name": "semantic_cache",
"config": {
"dimension": 1,
"ttl": 300,
"threshold": 0.8
}
}
]
}

View File

@@ -0,0 +1,30 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"vector_store": {
"enabled": true,
"type": "redis",
"config": {
"addr": "env.REDIS_ADDR",
"username": "env.REDIS_USERNAME",
"password": "env.REDIS_PASSWORD",
"db": 0,
"use_tls": true,
"insecure_skip_verify": false,
"ca_cert_pem": "env.REDIS_CA_CERT_PEM",
"cluster_mode": true
}
},
"plugins": [
{
"enabled": true,
"name": "semantic_cache",
"config": {
"dimension": 1,
"ttl": 300,
"threshold": 0.8,
"default_cache_key": "valkey-repro-cache",
"vector_store_namespace": "ValkeySemanticCacheRepro"
}
}
]
}

View File

@@ -0,0 +1,76 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"config_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "./config.db"
}
},
"logs_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "./logs.db"
}
},
"providers": {
"openai": {
"keys": [
{
"name": "openai-key-1",
"value": "sk-dummy",
"weight": 1,
"models": ["*"]
}
]
}
},
"governance": {
"customers": [
{
"id": "cust-acme",
"name": "Acme Corporation",
"budget_id": "b-cust-acme",
"rate_limit_id": "rl-cust-acme"
}
],
"teams": [
{
"id": "team-eng",
"name": "Engineering",
"customer_id": "cust-acme",
"budget_id": "b-team-eng"
},
{
"id": "team-sales",
"name": "Sales",
"customer_id": "cust-acme",
"budget_id": "b-team-sales"
}
],
"virtual_keys": [
{
"id": "vk-eng-1",
"name": "Engineering VK",
"value": "sk-bf-eng-1",
"is_active": true,
"team_id": "team-eng",
"rate_limit_id": "rl-vk-eng"
}
],
"budgets": [
{"id": "b-cust-acme", "max_limit": 5000, "reset_duration": "1M", "calendar_aligned": true},
{"id": "b-team-eng", "max_limit": 1000, "reset_duration": "1M"},
{"id": "b-team-sales", "max_limit": 500, "reset_duration": "1M"}
],
"rate_limits": [
{"id": "rl-cust-acme", "token_max_limit": 10000000, "token_reset_duration": "1h", "request_max_limit": 10000, "request_reset_duration": "1h"},
{"id": "rl-vk-eng", "token_max_limit": 500000, "token_reset_duration": "1h", "request_max_limit": 500, "request_reset_duration": "1h"}
],
"model_configs": [
{"id": "mc-gpt4", "model_name": "gpt-4o", "provider": "openai", "budget_id": "b-team-eng"},
{"id": "mc-gpt35", "model_name": "gpt-3.5-turbo", "provider": "openai", "rate_limit_id": "rl-vk-eng"}
]
}
}

View File

@@ -0,0 +1,33 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"config_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": ".config.db"
}
},
"logs_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": ".logs.db"
}
},
"vector_store": {
"enabled": true,
"type": "weaviate",
"config": {
"scheme": "http",
"host": "localhost:9000",
"grpc_config": {
"host": "localhost:50051",
"secured": false
}
}
},
"client": {
"enable_logging": true,
"log_retention_days": 30
}
}

View File

@@ -0,0 +1,23 @@
version: "3.8"
services:
weaviate:
image: cr.weaviate.io/semitechnologies/weaviate:1.32.4
container_name: weaviate
ports:
- "9000:8080"
- "50051:50051"
environment:
QUERY_DEFAULTS_LIMIT: 25
AUTHENTICATION_APIKEY_ENABLED: "false"
PERSISTENCE_DATA_PATH: "/var/lib/weaviate"
volumes:
- weaviate_data:/var/lib/weaviate
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/v1/.well-known/ready"]
interval: 10s
timeout: 5s
retries: 5
volumes:
weaviate_data:

View File

@@ -0,0 +1,314 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"client": {
"allow_direct_keys": false,
"allowed_origins": [
"*"
],
"disable_content_logging": false,
"drop_excess_requests": false,
"enable_logging": true,
"enforce_auth_on_inference": true,
"initial_pool_size": 300,
"log_retention_days": 365,
"max_request_body_size_mb": 100
},
"config_store": {
"config": {
"path": "../../examples/configs/withvirtualkeys/config.db"
},
"enabled": true,
"type": "sqlite"
},
"framework": {
"pricing": {
"pricing_sync_interval": 86400
}
},
"governance": {
"auth_config": {
"admin_password": "env.BIFROST_ADMIN_PASSWORD",
"admin_username": "env.BIFROST_ADMIN_USERNAME",
"disable_auth_on_inference": true,
"is_enabled": false
},
"virtual_keys": [
{
"id": "sk-bf-vk-prod-assistant-us-01",
"is_active": true,
"name": "prod-assistant-us-key-01-configurations",
"provider_configs": [
{
"key_ids": [
"key-azure-us-1-prod"
],
"allowed_models": [
"*"
],
"provider": "azure",
"weight": 0.5
},
{
"key_ids": [
"key-vertex-us-east1-prod",
"key-vertex-global-prod"
],
"allowed_models": [
"*"
],
"provider": "vertex",
"weight": 0.5
},
{
"key_ids": [
"key-openai-us-1-prod"
],
"allowed_models": [
"*"
],
"provider": "openai",
"weight": 0.5
}
],
"value": "env.BIFROST_VK_PROD_ASSISTANT_US_01"
},
{
"id": "sk-bf-vk-prod-assistant-eu-01",
"is_active": true,
"name": "prod-assistant-eu-key-01-configurations",
"provider_configs": [
{
"key_ids": [
"key-azure-eu-1-prod"
],
"allowed_models": [
"*"
],
"provider": "azure",
"weight": 0.5
},
{
"key_ids": [
"key-vertex-eu-west1-prod",
"key-vertex-global-prod"
],
"allowed_models": [
"*"
],
"provider": "vertex",
"weight": 0.5
},
{
"key_ids": [
"key-bedrock-eu-central-1-prod"
],
"allowed_models": [
"*"
],
"provider": "bedrock",
"weight": 0.5
}
],
"value": "sk-bf-vk-prod-assistant-eu-01"
}
]
},
"logs_store": {
"config": {
"path": "../../examples/configs/withvirtualkeys/logs.db"
},
"enabled": true,
"type": "sqlite"
},
"plugins": [
{
"config": {
"is_vk_mandatory": true
},
"enabled": true,
"name": "governance"
}
],
"providers": {
"azure": {
"keys": [
{
"id": "key-azure-us-1-prod",
"azure_key_config": {
"api_version": "2025-03-01-preview",
"endpoint": "https://orca-prod-us-east-1.openai.azure.com/"
},
"models": [
"gpt-4.1",
"gpt-4.1-mini",
"gpt-4o",
"gpt-4o-mini",
"gpt-5",
"gpt-5-mini",
"gpt-5.1"
],
"name": "azure-us-key-1-prod",
"value": "env.AZURE_OPENAI_API_KEY_US_EAST_1",
"weight": 1
},
{
"id": "key-azure-us-2-prod",
"azure_key_config": {
"api_version": "2025-03-01-preview",
"endpoint": "https://orca-prod-us-east-2.openai.azure.com/"
},
"models": [
"gpt-4.1",
"gpt-4.1-mini",
"gpt-4o",
"gpt-4o-mini",
"gpt-5",
"gpt-5-mini",
"gpt-5.1"
],
"name": "azure-us-key-2-prod",
"value": "env.AZURE_OPENAI_API_KEY_US_EAST_2",
"weight": 1
},
{
"id": "key-azure-eu-1-prod",
"azure_key_config": {
"api_version": "2025-03-01-preview",
"endpoint": "https://orca-prod-eu-central-1.openai.azure.com/"
},
"models": [
"gpt-4.1",
"gpt-4.1-mini",
"gpt-4o",
"gpt-4o-mini",
"gpt-5",
"gpt-5-mini",
"gpt-5.1"
],
"name": "azure-eu-key-1-prod",
"value": "env.AZURE_OPENAI_API_KEY_EU_CENTRAL_1",
"weight": 1
}
]
},
"bedrock": {
"keys": [
{
"id": "key-bedrock-us-east-1-prod",
"bedrock_key_config": {
"access_key": "env.AWS_ACCESS_KEY_ID_US_EAST_1",
"region": "us-east-1",
"secret_key": "env.AWS_SECRET_ACCESS_KEY_US_EAST_1"
},
"models": [
"*"
],
"name": "bedrock-us-east-1-prod",
"weight": 1
},
{
"id": "key-bedrock-us-west-2-prod",
"bedrock_key_config": {
"access_key": "env.AWS_ACCESS_KEY_ID_US_WEST_2",
"region": "us-west-2",
"secret_key": "env.AWS_SECRET_ACCESS_KEY_US_WEST_2"
},
"models": [
"*"
],
"name": "bedrock-us-west-2-prod",
"weight": 1
},
{
"id": "key-bedrock-eu-central-1-prod",
"bedrock_key_config": {
"access_key": "env.AWS_ACCESS_KEY_ID_EU_CENTRAL_1",
"region": "eu-central-1",
"secret_key": "env.AWS_SECRET_ACCESS_KEY_EU_CENTRAL_1"
},
"models": [
"*"
],
"name": "bedrock-eu-central-1-prod",
"weight": 1
}
]
},
"vertex": {
"keys": [
{
"id": "key-vertex-us-east1-prod",
"models": [
"google/gemini-2.5-pro",
"google/gemini-2.5-flash-lite",
"google/gemini-2.5-flash"
],
"name": "vertex-us-east1-prod",
"vertex_key_config": {
"auth_credentials": "env.VERTEX_CREDENTIALS_US_EAST1",
"project_id": "agentic-ai-project-1",
"region": "us-east1"
},
"weight": 1
},
{
"id": "key-vertex-eu-west1-prod",
"models": [
"google/gemini-2.5-pro",
"google/gemini-2.5-flash-lite",
"google/gemini-2.5-flash"
],
"name": "vertex-europe-west1-prod",
"vertex_key_config": {
"auth_credentials": "env.VERTEX_CREDENTIALS_EUROPE_WEST1",
"project_id": "agentic-ai-project-1",
"region": "europe-west1"
},
"weight": 1
},
{
"id": "key-vertex-us-west1-prod",
"models": [
"google/gemini-2.5-pro",
"google/gemini-2.5-flash-lite",
"google/gemini-2.5-flash"
],
"name": "vertex-us-west1-prod",
"vertex_key_config": {
"auth_credentials": "env.VERTEX_CREDENTIALS_US_WEST1",
"project_id": "agentic-ai-project-1",
"region": "us-west1"
},
"weight": 1
},
{
"id": "key-vertex-global-prod",
"models": [
"google/gemini-3-pro-preview",
"google/gemini-3-flash-preview"
],
"name": "vertex-global-prod",
"vertex_key_config": {
"auth_credentials": "env.VERTEX_CREDENTIALS_GLOBAL",
"project_id": "agentic-ai-project-1",
"region": "global"
},
"weight": 1
}
]
},
"openai": {
"keys": [
{
"id": "key-openai-us-1-prod",
"name": "openai-us-key-1-prod",
"value": "env.OPENAI_API_KEY_US_EAST_1",
"weight": 1,
"models": [
"*"
]
}
]
}
}
}

View File

@@ -0,0 +1,32 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"config_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "./config.db"
}
},
"websocket": {
"max_connections_per_user": 100,
"transcript_buffer_size": 100,
"pool": {
"max_idle_per_key": 50,
"max_total_connections": 1000,
"idle_timeout_seconds": 600,
"max_connection_lifetime_seconds": 7200
}
},
"providers": {
"openai": {
"keys": [
{
"name": "openai-key-1",
"value": "sk-dummy",
"weight": 1,
"models": ["*"]
}
]
}
}
}

View File

@@ -0,0 +1,40 @@
{
"$schema": "https://www.getbifrost.ai/schema",
"config_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "/app/data/config.db"
}
},
"logs_store": {
"enabled": true,
"type": "sqlite",
"config": {
"path": "/app/data/logs.db"
}
},
"client": {
"drop_excess_requests": false,
"initial_pool_size": 5000,
"prometheus_labels": [],
"enable_logging": true,
"disable_content_logging": false,
"log_retention_days": 365,
"enforce_auth_on_inference": false,
"allow_direct_keys": false,
"allowed_origins": [
"*"
],
"max_request_body_size_mb": 100,
"compat": {
"should_convert_params": false
}
},
"framework": {
"pricing": {
"pricing_url": "https://getbifrost.ai/datasheet",
"pricing_sync_interval": 86400
}
}
}

View File

@@ -0,0 +1,36 @@
services:
bifrost:
image: maximhq/bifrost:v1.4.3
container_name: bifrost
ports:
- "8080:8080"
volumes:
- ./data:/app/data
environment:
- APP_PORT=8080
- APP_HOST=0.0.0.0
- LOG_LEVEL=info
- LOG_STYLE=json
# Go runtime performance tuning (uncomment and adjust for your workload):
# - GOGC=200 # Higher = less GC, more memory (default: 100)
# - GOMEMLIMIT=3600MiB # Set to ~90% of container memory limit
# File descriptor limits for high-concurrency workloads
ulimits:
nofile:
soft: 65536
hard: 65536
# Resource limits (uncomment and adjust based on your infrastructure)
# deploy:
# resources:
# limits:
# cpus: '4'
# memory: 4G
# reservations:
# cpus: '2'
# memory: 2G
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "-O", "/dev/null", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
restart: unless-stopped

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

View File

@@ -0,0 +1,74 @@
.PHONY: all build clean help check-tinygo
# Colors
COLOR_RESET = \033[0m
COLOR_INFO = \033[36m
COLOR_SUCCESS = \033[32m
COLOR_WARNING = \033[33m
COLOR_ERROR = \033[31m
COLOR_BOLD = \033[1m
# Plugin configuration
PLUGIN_NAME = hello-world
OUTPUT_DIR = build
OUTPUT = $(OUTPUT_DIR)/$(PLUGIN_NAME).wasm
# TinyGo build flags
TINYGO_TARGET = wasi
TINYGO_SCHEDULER = none
help: ## Show this help message
@echo '$(COLOR_BOLD)Hello World WASM Plugin$(COLOR_RESET)'
@echo ''
@echo '$(COLOR_BOLD)Usage:$(COLOR_RESET) make [target]'
@echo ''
@echo '$(COLOR_BOLD)Prerequisites:$(COLOR_RESET)'
@echo ' - TinyGo (https://tinygo.org/getting-started/install/)'
@echo ' macOS: brew install tinygo'
@echo ' Linux: See TinyGo installation docs'
@echo ''
@echo '$(COLOR_BOLD)Available targets:$(COLOR_RESET)'
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " $(COLOR_INFO)%-15s$(COLOR_RESET) %s\n", $$1, $$2}' $(MAKEFILE_LIST)
check-tinygo: ## Check if TinyGo is installed
@which tinygo > /dev/null 2>&1 || (echo "$(COLOR_ERROR)Error: TinyGo is not installed$(COLOR_RESET)"; \
echo "$(COLOR_INFO)Install TinyGo:$(COLOR_RESET)"; \
echo " macOS: brew install tinygo"; \
echo " Linux: See https://tinygo.org/getting-started/install/"; \
exit 1)
@echo "$(COLOR_SUCCESS)✓ TinyGo found: $$(tinygo version)$(COLOR_RESET)"
build: check-tinygo ## Build the WASM plugin
@mkdir -p $(OUTPUT_DIR)
@echo "$(COLOR_INFO)Building WASM plugin...$(COLOR_RESET)"
GOWORK=off tinygo build -o $(OUTPUT) -target=$(TINYGO_TARGET) -scheduler=$(TINYGO_SCHEDULER) .
@echo "$(COLOR_SUCCESS)✓ Plugin built successfully: $(OUTPUT)$(COLOR_RESET)"
@ls -lh $(OUTPUT) | awk '{print " Size: " $$5}'
build-optimized: check-tinygo ## Build the WASM plugin with size optimizations
@mkdir -p $(OUTPUT_DIR)
@echo "$(COLOR_INFO)Building optimized WASM plugin...$(COLOR_RESET)"
GOWORK=off tinygo build -o $(OUTPUT) -target=$(TINYGO_TARGET) -scheduler=$(TINYGO_SCHEDULER) -no-debug -gc=leaking .
@echo "$(COLOR_SUCCESS)✓ Optimized plugin built: $(OUTPUT)$(COLOR_RESET)"
@ls -lh $(OUTPUT) | awk '{print " Size: " $$5}'
clean: ## Remove build artifacts
@echo "$(COLOR_INFO)Cleaning build artifacts...$(COLOR_RESET)"
@rm -rf $(OUTPUT_DIR)
@echo "$(COLOR_SUCCESS)✓ Clean complete$(COLOR_RESET)"
info: ## Show build information
@echo "$(COLOR_BOLD)Build Configuration$(COLOR_RESET)"
@echo " Plugin Name: $(PLUGIN_NAME)"
@echo " Output: $(OUTPUT)"
@echo " Target: $(TINYGO_TARGET)"
@echo " Scheduler: $(TINYGO_SCHEDULER)"
@echo ""
@if [ -f "$(OUTPUT)" ]; then \
echo "$(COLOR_SUCCESS)Plugin exists:$(COLOR_RESET)"; \
ls -lh $(OUTPUT) | awk '{print " " $$9 " (" $$5 ")"}'; \
else \
echo "$(COLOR_WARNING)Plugin not built yet$(COLOR_RESET)"; \
fi
.DEFAULT_GOAL := help

View File

@@ -0,0 +1,170 @@
# Hello World WASM Plugin
A minimal example of a Bifrost plugin written in Go and compiled to WebAssembly using TinyGo.
## Prerequisites
### TinyGo Installation
TinyGo is required to compile Go code to WebAssembly with a small binary size.
**macOS:**
```bash
brew install tinygo
```
**Linux (Ubuntu/Debian):**
```bash
wget https://github.com/tinygo-org/tinygo/releases/download/v0.32.0/tinygo_0.32.0_amd64.deb
sudo dpkg -i tinygo_0.32.0_amd64.deb
```
**Other platforms:**
See [TinyGo Installation Guide](https://tinygo.org/getting-started/install/)
## Building
```bash
# Build the WASM plugin
make build
# Build with size optimizations
make build-optimized
# Clean build artifacts
make clean
```
The compiled plugin will be at `build/hello-world.wasm`.
## Plugin Structure
WASM plugins must export the following functions:
| Export | Signature | Description |
|--------|-----------|-------------|
| `plugin_malloc` | `(size: u32) -> u32` | Allocate memory for host to write data (or `malloc` for non-TinyGo) |
| `plugin_free` | `(ptr: u32)` | Free allocated memory (optional, or `free` for non-TinyGo) |
| `get_name` | `() -> u64` | Returns packed ptr+len of plugin name |
| `http_transport_intercept` | `(ctx_ptr, ctx_len, req_ptr, req_len: u32) -> u64` | HTTP transport intercept |
| `pre_hook` | `(ctx_ptr, ctx_len, req_ptr, req_len: u32) -> u64` | Pre-request hook |
| `post_hook` | `(ctx_ptr, ctx_len, resp_ptr, resp_len, err_ptr, err_len: u32) -> u64` | Post-response hook |
| `cleanup` | `() -> i32` | Cleanup resources (0 = success) |
| `init` | `(config_ptr, config_len: u32) -> i32` | Initialize with config (optional) |
### Return Value Format
Functions returning data use a packed `u64` format:
- Upper 32 bits: pointer to data in WASM memory
- Lower 32 bits: length of data
### Data Exchange
All complex data is exchanged as JSON:
**HTTPTransportIntercept Input:**
- `ctx`: `{"request_id": "..."}` (context info)
- `req`: HTTP request JSON
```json
{
"method": "POST",
"path": "/v1/chat/completions",
"headers": {"Content-Type": "application/json"},
"query": {},
"body": "base64-encoded-body"
}
```
**HTTPTransportIntercept Output:**
```json
{
"response": null,
"error": ""
}
```
To short-circuit, return a response:
```json
{
"response": {
"status_code": 401,
"headers": {"Content-Type": "application/json"},
"body": "base64-encoded-body"
},
"error": ""
}
```
**PreLLMHook Input:**
- `ctx`: `{"request_id": "..."}` (context info)
- `req`: Bifrost request JSON
**PreLLMHook Output:**
```json
{
"request": { ... },
"short_circuit": null,
"error": ""
}
```
**PostLLMHook Input:**
- `ctx`: Context JSON
- `resp`: Bifrost response JSON
- `err`: Bifrost error JSON (or null)
**PostLLMHook Output:**
```json
{
"response": { ... },
"bifrost_error": null,
"error": ""
}
```
## Usage with Bifrost
Configure the plugin in your Bifrost config:
```json
{
"plugins": [
{
"path": "/path/to/hello-world.wasm",
"name": "hello-world-wasm",
"enabled": true
}
]
}
```
Or load from URL:
```json
{
"plugins": [
{
"path": "https://example.com/plugins/hello-world.wasm",
"name": "hello-world-wasm",
"enabled": true
}
]
}
```
## Limitations
WASM plugins have some limitations compared to native `.so` plugins:
1. **Performance**: JSON serialization/deserialization adds overhead compared to native plugins.
2. **Memory**: WASM modules have a linear memory model with limited addressing.
3. **TinyGo Constraints**: Some Go standard library features are not available in TinyGo.
## Benefits
1. **Cross-platform**: Single `.wasm` binary runs on any OS/architecture
2. **Security**: WASM provides sandboxed execution
3. **No CGO**: Pure Go compilation, no C dependencies needed on the host
4. **Portability**: Easy to distribute and deploy
5. **Full feature parity**: HTTP transport intercept, PreLLMHook, and PostLLMHook all supported

View File

@@ -0,0 +1,30 @@
module github.com/maximhq/bifrost/examples/plugins/hello-world-wasm
go 1.26.2
require github.com/maximhq/bifrost/core v1.4.17
require (
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.2 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/mark3labs/mcp-go v0.43.2 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.68.0 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/sys v0.42.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -0,0 +1,83 @@
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
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/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/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/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
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.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
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/maximhq/bifrost/core v1.3.3 h1:r2llMAfzIHeSxwY2L55UaSOsY17JSg5zYcqF2JtaRVY=
github.com/maximhq/bifrost/core v1.3.3/go.mod h1:abKQRnJQPZz8/UMxCcbuNHEyq19Db+IX4KlGJdlLY8E=
github.com/maximhq/bifrost/core v1.4.17/go.mod h1:O6VEP2MHkQgo1iLYoxGQ7a+3VBBlHoETCH+pOR6Q5X8=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
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/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
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=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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,226 @@
// Package main provides a hello-world WASM plugin example for Bifrost.
// This plugin demonstrates the basic structure and exports required for WASM plugins.
//
// Build with TinyGo:
//
// tinygo build -o build/hello-world.wasm -target=wasi -scheduler=none main.go
package main
import (
"encoding/json"
)
// ============================================================================
// Plugin Exports
// ============================================================================
//export get_name
func get_name() uint64 {
return writeBytes([]byte("Hello World WASM Plugin"))
}
//export init
func init_plugin(configPtr, configLen uint32) int32 {
println("WASM Plugin: Init called")
if configLen > 0 {
configData := readInput(configPtr, configLen)
println("WASM Plugin: Config received:", string(configData))
}
return 0
}
//export http_intercept
func http_intercept(inputPtr, inputLen uint32) uint64 {
println("WASM Plugin: http_intercept called")
inputData := readInput(inputPtr, inputLen)
if inputData == nil {
return writeError("no input data")
}
// Parse input
var input HTTPInterceptInput
if err := json.Unmarshal(inputData, &input); err != nil {
println("WASM Plugin: parse error:", err.Error())
return writeError("parse error: " + err.Error())
}
// Log parsed data
println("WASM Plugin: HTTP", input.Request.Method, input.Request.Path)
if ct, ok := input.Request.Headers["content-type"]; ok {
println("WASM Plugin: Content-Type:", ct)
}
input.Context["from-http"] = "123"
// Return pass-through
output := HTTPInterceptOutput{
Context: input.Context,
Request: input.Request,
HasResponse: false,
Error: "",
}
data, _ := json.Marshal(output)
return writeBytes(data)
}
//export pre_hook
func pre_hook(inputPtr, inputLen uint32) uint64 {
println("WASM Plugin: pre_hook called")
inputData := readInput(inputPtr, inputLen)
if inputData == nil {
return writePreHookError("no input data")
}
println("WASM Plugin: Pre-hook input:", string(inputData))
// Parse input
var input PreHookInput
if err := json.Unmarshal(inputData, &input); err != nil {
println("WASM Plugin: parse error:", err.Error())
return writePreHookError("parse error: " + err.Error())
}
// Print existing context
for k, v := range input.Context {
println("WASM Plugin: Context", k, "=", v)
}
input.Context["from-pre-hook"] = "789"
// Return with custom context value
output := PreHookOutput{
Context: input.Context,
Request: input.Request,
HasShortCircuit: false,
Error: "",
}
data, _ := json.Marshal(output)
return writeBytes(data)
}
//export post_hook
func post_hook(inputPtr, inputLen uint32) uint64 {
println("WASM Plugin: post_hook called")
inputData := readInput(inputPtr, inputLen)
if inputData == nil {
return writePostHookError("no input data")
}
// Parse input
var input PostHookInput
if err := json.Unmarshal(inputData, &input); err != nil {
println("WASM Plugin: parse error:", err.Error())
return writePostHookError("parse error: " + err.Error())
}
println("WASM Plugin: Post-hook input:", string(inputData))
// Print existing context
for k, v := range input.Context {
println("WASM Plugin: Context", k, "=", v)
}
// Parse response for logging
if processed, ok := input.Context["wasm_plugin_processed"].(bool); ok && processed {
println("WASM Plugin: Pre-hook context value present")
}
input.Context["from-post-hook"] = "456"
// Return pass-through
output := PostHookOutput{
Context: input.Context,
Response: input.Response,
Error: input.Error,
HasError: false,
HookError: "",
}
data, _ := json.Marshal(output)
return writeBytes(data)
}
//export http_stream_chunk_hook
func http_stream_chunk_hook(inputPtr, inputLen uint32) uint64 {
println("WASM Plugin: http_stream_chunk_hook called")
inputData := readInput(inputPtr, inputLen)
if inputData == nil {
return writeStreamChunkError("no input data")
}
// Parse input
var input HTTPStreamChunkHookInput
if err := json.Unmarshal(inputData, &input); err != nil {
println("WASM Plugin: parse error:", err.Error())
return writeStreamChunkError("parse error: " + err.Error())
}
println("WASM Plugin: Stream chunk received")
// Add context value
input.Context["from-stream-chunk"] = "wasm-plugin"
// Pass through chunk unchanged
output := HTTPStreamChunkHookOutput{
Context: input.Context,
Chunk: input.Chunk,
HasChunk: true,
Skip: false,
Error: "",
}
data, _ := json.Marshal(output)
return writeBytes(data)
}
//export cleanup
func cleanup() int32 {
println("WASM Plugin: Cleanup called")
return 0
}
// Helper functions for error responses
func writeError(msg string) uint64 {
output := HTTPInterceptOutput{HasResponse: false, Error: msg}
data, _ := json.Marshal(output)
return writeBytes(data)
}
func writePreHookError(msg string) uint64 {
output := PreHookOutput{
Context: map[string]interface{}{},
Request: nil,
HasShortCircuit: false,
Error: msg,
}
data, _ := json.Marshal(output)
return writeBytes(data)
}
func writePostHookError(msg string) uint64 {
output := PostHookOutput{
Context: map[string]interface{}{},
Response: nil,
HasError: false,
HookError: msg,
}
data, _ := json.Marshal(output)
return writeBytes(data)
}
func writeStreamChunkError(msg string) uint64 {
output := HTTPStreamChunkHookOutput{
Context: map[string]interface{}{},
Chunk: nil,
HasChunk: false,
Skip: false,
Error: msg,
}
data, _ := json.Marshal(output)
return writeBytes(data)
}
func main() {}

View File

@@ -0,0 +1,89 @@
package main
import "unsafe"
// ============================================================================
// Memory Management
// ============================================================================
// heapSize is the fixed size of the pre-allocated heap.
// This must be large enough to handle all allocations during the plugin lifetime.
// The heap is never reallocated to ensure all pointers remain valid.
const heapSize = 4 * 1024 * 1024 // 4MB fixed heap
// heapBase is a fixed-size buffer that is never reallocated.
// All allocations come from this buffer to ensure pointer stability.
var heapBase []byte
// heapOffset tracks the next available position in heapBase.
var heapOffset uint32 = 0
// heapBasePtr caches the base pointer of heapBase for efficient offset-to-pointer conversion.
var heapBasePtr uintptr
func init() {
// Pre-allocate the fixed heap once at startup.
// This ensures heapBase is never reallocated after pointers are handed out.
heapBase = make([]byte, heapSize)
heapBasePtr = uintptr(unsafe.Pointer(&heapBase[0]))
}
//export plugin_malloc
func plugin_malloc(size uint32) uint32 {
if size == 0 {
return 0
}
// Align to 8-byte boundary
alignedSize := (size + 7) &^ 7
// Check if we have enough space (no reallocation allowed)
if heapOffset+alignedSize > uint32(len(heapBase)) {
// Allocation failure - heap exhausted
// Return 0 to indicate failure rather than reallocating
return 0
}
// Return pointer to the allocated region
ptr := uint32(heapBasePtr + uintptr(heapOffset))
heapOffset += alignedSize
return ptr
}
//export plugin_free
func plugin_free(ptr uint32) {
// No-op: we use a simple bump allocator without individual frees.
// Memory is reclaimed when the plugin is unloaded.
}
// plugin_reset resets the heap allocator, allowing memory to be reused.
// This should only be called when no allocated memory is in use.
//
//export plugin_reset
func plugin_reset() {
heapOffset = 0
}
func packResult(ptr uint32, length uint32) uint64 {
return (uint64(ptr) << 32) | uint64(length)
}
func writeBytes(data []byte) uint64 {
if len(data) == 0 {
return 0
}
// Allocate from the stable heap
ptr := plugin_malloc(uint32(len(data)))
if ptr == 0 {
// Allocation failed
return 0
}
// Copy data into the allocated region
offset := ptr - uint32(heapBasePtr)
copy(heapBase[offset:offset+uint32(len(data))], data)
return packResult(ptr, uint32(len(data)))
}
func readInput(ptr, length uint32) []byte {
if length == 0 {
return nil
}
return unsafe.Slice((*byte)(unsafe.Pointer(uintptr(ptr))), length)
}

View File

@@ -0,0 +1,70 @@
package main
import "github.com/maximhq/bifrost/core/schemas"
// ============================================================================
// Input/Output Structs
// ============================================================================
// HTTPInterceptInput is the input for http_intercept
type HTTPInterceptInput struct {
Context map[string]interface{} `json:"context"`
Request *schemas.HTTPRequest `json:"request,omitempty"`
}
// HTTPInterceptOutput is the output for http_intercept
type HTTPInterceptOutput struct {
Context map[string]interface{} `json:"context"`
Request *schemas.HTTPRequest `json:"request,omitempty"`
Response *schemas.HTTPResponse `json:"response,omitempty"`
HasResponse bool `json:"has_response"`
Error string `json:"error"`
}
// PreHookInput is the input for pre_hook
type PreHookInput struct {
Context map[string]interface{} `json:"context"`
Request *schemas.BifrostRequest `json:"request,omitempty"` // Keep raw for pass-through
}
// PreHookOutput is the output for pre_hook
type PreHookOutput struct {
Context map[string]interface{} `json:"context"`
Request *schemas.BifrostRequest `json:"request,omitempty"`
ShortCircuit *schemas.LLMPluginShortCircuit `json:"short_circuit,omitempty"`
HasShortCircuit bool `json:"has_short_circuit"`
Error string `json:"error"`
}
// PostHookInput is the input for post_hook
type PostHookInput struct {
Context map[string]interface{} `json:"context"`
Response *schemas.BifrostResponse `json:"response,omitempty"`
Error *schemas.BifrostError `json:"error,omitempty"`
HasError bool `json:"has_error"`
}
// PostHookOutput is the output for post_hook
type PostHookOutput struct {
Context map[string]interface{} `json:"context"`
Response *schemas.BifrostResponse `json:"response,omitempty"`
Error *schemas.BifrostError `json:"error,omitempty"`
HasError bool `json:"has_error"`
HookError string `json:"hook_error"`
}
// HTTPStreamChunkHookInput is the input for http_stream_chunk_hook
type HTTPStreamChunkHookInput struct {
Context map[string]interface{} `json:"context"`
Request *schemas.HTTPRequest `json:"request,omitempty"`
Chunk *schemas.BifrostStreamChunk `json:"chunk,omitempty"`
}
// HTTPStreamChunkHookOutput is the output for http_stream_chunk_hook
type HTTPStreamChunkHookOutput struct {
Context map[string]interface{} `json:"context"`
Chunk *schemas.BifrostStreamChunk `json:"chunk,omitempty"`
HasChunk bool `json:"has_chunk"`
Skip bool `json:"skip"`
Error string `json:"error"`
}

View File

@@ -0,0 +1,107 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "hello-world-wasm-rust"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "itoa"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "proc-macro2"
version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "syn"
version = "2.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "zmij"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8"

View File

@@ -0,0 +1,18 @@
[package]
name = "hello-world-wasm-rust"
version = "0.1.0"
edition = "2021"
description = "A minimal Bifrost WASM plugin example in Rust"
[lib]
crate-type = ["cdylib"]
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[profile.release]
opt-level = "s"
lto = true
strip = true
panic = "abort"

View File

@@ -0,0 +1,80 @@
.PHONY: all build build-optimized clean help check-rust
# Colors
COLOR_RESET = \033[0m
COLOR_INFO = \033[36m
COLOR_SUCCESS = \033[32m
COLOR_WARNING = \033[33m
COLOR_ERROR = \033[31m
COLOR_BOLD = \033[1m
# Plugin configuration
PLUGIN_NAME = hello-world
OUTPUT_DIR = build
OUTPUT = $(OUTPUT_DIR)/$(PLUGIN_NAME).wasm
TARGET = wasm32-unknown-unknown
help: ## Show this help message
@echo '$(COLOR_BOLD)Hello World WASM Plugin (Rust)$(COLOR_RESET)'
@echo ''
@echo '$(COLOR_BOLD)Usage:$(COLOR_RESET) make [target]'
@echo ''
@echo '$(COLOR_BOLD)Prerequisites:$(COLOR_RESET)'
@echo ' - Rust with wasm32-unknown-unknown target'
@echo ' rustup target add wasm32-unknown-unknown'
@echo ''
@echo '$(COLOR_BOLD)Available targets:$(COLOR_RESET)'
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " $(COLOR_INFO)%-15s$(COLOR_RESET) %s\n", $$1, $$2}' $(MAKEFILE_LIST)
check-rust: ## Check if Rust and WASM target are installed
@which cargo > /dev/null 2>&1 || (echo "$(COLOR_ERROR)Error: Rust/Cargo is not installed$(COLOR_RESET)"; \
echo "$(COLOR_INFO)Install Rust: https://rustup.rs/$(COLOR_RESET)"; \
exit 1)
@rustup target list --installed | grep -q $(TARGET) || (echo "$(COLOR_ERROR)Error: WASM target not installed$(COLOR_RESET)"; \
echo "$(COLOR_INFO)Install with: rustup target add $(TARGET)$(COLOR_RESET)"; \
exit 1)
@echo "$(COLOR_SUCCESS)✓ Rust found: $$(rustc --version)$(COLOR_RESET)"
@echo "$(COLOR_SUCCESS)✓ WASM target: $(TARGET)$(COLOR_RESET)"
build: check-rust ## Build the WASM plugin
@mkdir -p $(OUTPUT_DIR)
@echo "$(COLOR_INFO)Building WASM plugin...$(COLOR_RESET)"
cargo build --release --target $(TARGET)
@cp target/$(TARGET)/release/hello_world_wasm_rust.wasm $(OUTPUT)
@echo "$(COLOR_SUCCESS)✓ Plugin built successfully: $(OUTPUT)$(COLOR_RESET)"
@ls -lh $(OUTPUT) | awk '{print " Size: " $$5}'
build-optimized: check-rust ## Build with wasm-opt optimization (requires wasm-opt)
@mkdir -p $(OUTPUT_DIR)
@echo "$(COLOR_INFO)Building optimized WASM plugin...$(COLOR_RESET)"
cargo build --release --target $(TARGET)
@cp target/$(TARGET)/release/hello_world_wasm_rust.wasm $(OUTPUT)
@if which wasm-opt > /dev/null 2>&1; then \
echo "$(COLOR_INFO)Running wasm-opt...$(COLOR_RESET)"; \
wasm-opt -Os -o $(OUTPUT) $(OUTPUT); \
else \
echo "$(COLOR_WARNING)wasm-opt not found, skipping optimization$(COLOR_RESET)"; \
fi
@echo "$(COLOR_SUCCESS)✓ Plugin built: $(OUTPUT)$(COLOR_RESET)"
@ls -lh $(OUTPUT) | awk '{print " Size: " $$5}'
clean: ## Remove build artifacts
@echo "$(COLOR_INFO)Cleaning build artifacts...$(COLOR_RESET)"
@cargo clean
@rm -rf $(OUTPUT_DIR)
@echo "$(COLOR_SUCCESS)✓ Clean complete$(COLOR_RESET)"
info: ## Show build information
@echo "$(COLOR_BOLD)Build Configuration$(COLOR_RESET)"
@echo " Plugin Name: $(PLUGIN_NAME)"
@echo " Output: $(OUTPUT)"
@echo " Target: $(TARGET)"
@echo ""
@if [ -f "$(OUTPUT)" ]; then \
echo "$(COLOR_SUCCESS)Plugin exists:$(COLOR_RESET)"; \
ls -lh $(OUTPUT) | awk '{print " " $$9 " (" $$5 ")"}'; \
else \
echo "$(COLOR_WARNING)Plugin not built yet$(COLOR_RESET)"; \
fi
.DEFAULT_GOAL := help

View File

@@ -0,0 +1,528 @@
# Bifrost WASM Plugin (Rust)
A comprehensive example of a Bifrost plugin written in Rust and compiled to WebAssembly. This plugin demonstrates proper structure definitions with serde, JSON parsing, context handling, and request/response modification patterns.
## Prerequisites
### Rust Installation
Install Rust from [rustup.rs](https://rustup.rs/) and add the WASM target:
```bash
# Install Rust (if not already installed)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Add WASM target
rustup target add wasm32-unknown-unknown
```
### Optional: wasm-opt
For smaller binaries, install `wasm-opt` from [binaryen](https://github.com/WebAssembly/binaryen):
```bash
# macOS
brew install binaryen
# Linux
apt install binaryen
```
## Building
```bash
# Build the WASM plugin
make build
# Build with wasm-opt optimization
make build-optimized
# Clean build artifacts
make clean
```
The compiled plugin will be at `build/hello-world.wasm`.
## File Structure
```
src/
├── lib.rs # Plugin implementation (hooks)
├── memory.rs # Memory management utilities
└── types.rs # Type definitions (mirrors Go SDK)
```
## Plugin Structure
WASM plugins must export the following functions:
| Export | Signature | Description |
|--------|-----------|-------------|
| `malloc` | `(size: u32) -> u32` | Allocate memory for host to write data |
| `free` | `(ptr: u32, size: u32)` | Free allocated memory |
| `get_name` | `() -> u64` | Returns packed ptr+len of plugin name |
| `init` | `(config_ptr, config_len: u32) -> i32` | Initialize with config (optional) |
| `http_intercept` | `(input_ptr, input_len: u32) -> u64` | HTTP transport intercept |
| `pre_hook` | `(input_ptr, input_len: u32) -> u64` | Pre-request hook |
| `post_hook` | `(input_ptr, input_len: u32) -> u64` | Post-response hook |
| `cleanup` | `() -> i32` | Cleanup resources (0 = success) |
### Return Value Format
Functions returning data use a packed `u64` format:
- Upper 32 bits: pointer to data in WASM memory
- Lower 32 bits: length of data
## Data Structures
This plugin uses `serde` with derive macros for JSON serialization. All structures mirror the Go SDK types:
### Context
```rust
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BifrostContext {
pub request_id: Option<String>,
// Custom values via HashMap
#[serde(flatten)]
pub values: HashMap<String, serde_json::Value>,
}
impl BifrostContext {
pub fn set_value(&mut self, key: &str, value: impl Into<serde_json::Value>);
pub fn get_string(&self, key: &str) -> Option<&str>;
pub fn get_bool(&self, key: &str) -> Option<bool>;
}
```
### HTTP Transport Types
```rust
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct HTTPRequest {
pub method: String,
pub path: String,
pub headers: HashMap<String, String>,
pub query: HashMap<String, String>,
pub body: String, // base64 encoded
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct HTTPResponse {
pub status_code: i32,
pub headers: HashMap<String, String>,
pub body: String, // base64 encoded
}
```
### Chat Completion Types
```rust
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ChatMessageRole {
User,
Assistant,
System,
Tool,
Developer,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ChatMessageContent {
Text(String),
Blocks(Vec<ChatContentBlock>),
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ChatMessage {
pub role: ChatMessageRole,
pub content: Option<ChatMessageContent>,
pub name: Option<String>,
pub tool_call_id: Option<String>,
pub tool_calls: Option<Vec<ToolCall>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ChatParameters {
pub temperature: Option<f64>,
pub max_completion_tokens: Option<i32>,
pub top_p: Option<f64>,
pub frequency_penalty: Option<f64>,
pub presence_penalty: Option<f64>,
pub stop: Option<Vec<String>>,
pub tools: Option<Vec<ChatTool>>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BifrostChatRequest {
pub provider: String,
pub model: String,
pub input: Vec<ChatMessage>,
pub params: Option<ChatParameters>,
pub fallbacks: Option<Vec<Fallback>>,
}
```
### Response Types
```rust
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct LLMUsage {
pub prompt_tokens: i32,
pub completion_tokens: i32,
pub total_tokens: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ResponseChoice {
pub index: i32,
pub message: Option<ChatMessage>,
pub delta: Option<ChatMessage>,
pub finish_reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BifrostChatResponse {
pub id: String,
pub model: String,
pub choices: Vec<ResponseChoice>,
pub usage: Option<LLMUsage>,
pub created: Option<i64>,
pub object: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BifrostResponse {
pub chat_response: Option<BifrostChatResponse>,
}
```
### Error Types
```rust
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ErrorField {
pub message: String,
#[serde(rename = "type")]
pub error_type: Option<String>,
pub code: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BifrostError {
pub error: ErrorField,
pub status_code: Option<i32>,
pub allow_fallbacks: Option<bool>,
}
impl BifrostError {
pub fn new(message: &str) -> Self;
pub fn with_type(self, error_type: &str) -> Self;
pub fn with_code(self, code: &str) -> Self;
pub fn with_status(self, status: i32) -> Self;
}
```
### Short Circuit
```rust
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct LLMPluginShortCircuit {
pub response: Option<BifrostResponse>,
pub error: Option<BifrostError>,
}
```
## Hook Input/Output Structures
### http_intercept
**Input:**
```json
{
"context": { "request_id": "abc-123" },
"request": {
"method": "POST",
"path": "/v1/chat/completions",
"headers": { "Content-Type": "application/json" },
"query": {},
"body": "<base64-encoded>"
}
}
```
**Output:**
```json
{
"context": { "request_id": "abc-123" },
"request": {},
"response": { "status_code": 200, "headers": {}, "body": "<base64>" },
"has_response": false,
"error": ""
}
```
### pre_hook
**Input:**
```json
{
"context": { "request_id": "abc-123" },
"request": {
"provider": "openai",
"model": "gpt-4",
"input": [{ "role": "user", "content": "Hello" }],
"params": { "temperature": 0.7 }
}
}
```
**Output:**
```json
{
"context": { "request_id": "abc-123", "plugin_processed": true },
"request": {},
"short_circuit": {
"response": { "chat_response": { ... } }
},
"has_short_circuit": false,
"error": ""
}
```
### post_hook
**Input:**
```json
{
"context": { "request_id": "abc-123", "plugin_processed": true },
"response": {
"chat_response": {
"id": "chatcmpl-123",
"model": "gpt-4",
"choices": [{ "index": 0, "message": { "role": "assistant", "content": "Hi!" } }],
"usage": { "prompt_tokens": 5, "completion_tokens": 10, "total_tokens": 15 }
}
},
"error": {},
"has_error": false
}
```
**Output:**
```json
{
"context": { "request_id": "abc-123", "post_hook_completed": true },
"response": {},
"error": {},
"has_error": false,
"hook_error": ""
}
```
## Usage Examples
### Modifying Context
```rust
#[no_mangle]
pub extern "C" fn pre_hook(input_ptr: u32, input_len: u32) -> u64 {
let input_str = read_string(input_ptr, input_len);
let input: PreHookInput = serde_json::from_str(&input_str).unwrap();
let mut output = PreHookOutput {
context: input.context.clone(),
..Default::default()
};
// Add custom values to context
output.context.set_value("plugin_processed", serde_json::json!(true));
output.context.set_value("plugin_name", serde_json::json!("my-rust-plugin"));
write_string(&serde_json::to_string(&output).unwrap())
}
```
### Short-Circuit with Mock Response
```rust
#[no_mangle]
pub extern "C" fn pre_hook(input_ptr: u32, input_len: u32) -> u64 {
let input_str = read_string(input_ptr, input_len);
let input: PreHookInput = serde_json::from_str(&input_str).unwrap();
let (provider, model) = input.get_provider_model();
// Check if this should be mocked
if model == "mock-model" {
let mut output = PreHookOutput {
context: input.context.clone(),
has_short_circuit: true,
..Default::default()
};
// Build mock response
let mock_response = BifrostResponse {
chat_response: Some(BifrostChatResponse {
id: format!("mock-{}", input.context.request_id.unwrap_or_default()),
model: "mock-model".to_string(),
choices: vec![ResponseChoice {
index: 0,
message: Some(ChatMessage {
role: ChatMessageRole::Assistant,
content: Some(ChatMessageContent::Text(
"This is a mock response!".to_string()
)),
..Default::default()
}),
finish_reason: Some("stop".to_string()),
..Default::default()
}],
usage: Some(LLMUsage {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
output.short_circuit = Some(LLMPluginShortCircuit {
response: Some(mock_response),
error: None,
});
return write_string(&serde_json::to_string(&output).unwrap());
}
// Pass through
let output = PreHookOutput {
context: input.context,
..Default::default()
};
write_string(&serde_json::to_string(&output).unwrap())
}
```
### Short-Circuit with Error
```rust
#[no_mangle]
pub extern "C" fn pre_hook(input_ptr: u32, input_len: u32) -> u64 {
let input_str = read_string(input_ptr, input_len);
let input: PreHookInput = serde_json::from_str(&input_str).unwrap();
// Check rate limit (example)
if should_rate_limit(&input.context) {
let mut output = PreHookOutput {
context: input.context.clone(),
has_short_circuit: true,
..Default::default()
};
output.short_circuit = Some(LLMPluginShortCircuit {
response: None,
error: Some(
BifrostError::new("Rate limit exceeded")
.with_type("rate_limit")
.with_code("429")
.with_status(429)
),
});
return write_string(&serde_json::to_string(&output).unwrap());
}
// Pass through
let output = PreHookOutput {
context: input.context,
..Default::default()
};
write_string(&serde_json::to_string(&output).unwrap())
}
```
### Modifying Responses in post_hook
```rust
#[no_mangle]
pub extern "C" fn post_hook(input_ptr: u32, input_len: u32) -> u64 {
let input_str = read_string(input_ptr, input_len);
let input: PostHookInput = serde_json::from_str(&input_str).unwrap();
let mut output = PostHookOutput {
context: input.context.clone(),
..Default::default()
};
// Handle errors
if input.has_error {
output.has_error = true;
output.error = input.error.clone();
// Optionally modify the error
if let Some(mut error) = input.parse_error() {
error.error.message = format!("{} (via rust plugin)", error.error.message);
output.error = serde_json::to_value(&error).unwrap_or_default();
}
return write_string(&serde_json::to_string(&output).unwrap());
}
// Pass through or modify response
if let Some(mut response) = input.parse_response() {
if let Some(ref mut chat) = response.chat_response {
// Add a marker to the model name
chat.model = format!("{} (via rust-wasm)", chat.model);
}
output.response = serde_json::to_value(&response).unwrap_or_default();
}
write_string(&serde_json::to_string(&output).unwrap())
}
```
## Usage with Bifrost
Configure the plugin in your Bifrost config:
```json
{
"plugins": [
{
"path": "/path/to/hello-world.wasm",
"name": "hello-world-wasm-rust",
"enabled": true,
"config": {
"custom_option": "value"
}
}
]
}
```
## Testing
The plugin includes unit tests that can be run with:
```bash
cargo test
```
## Benefits
1. **Performance**: Rust compiles to highly optimized WASM
2. **Safety**: Memory safety without garbage collection
3. **Small binaries**: Rust WASM binaries are typically very small
4. **Cross-platform**: Single `.wasm` binary runs on any OS/architecture
5. **Security**: WASM provides sandboxed execution
6. **Type Safety**: Strongly typed structures with serde derive macros
7. **Excellent JSON**: serde_json provides robust JSON handling

View File

@@ -0,0 +1,327 @@
//! Bifrost WASM Plugin for Rust
//!
//! This plugin demonstrates the proper structure for parsing inputs,
//! building responses, and handling context - similar to Go plugin patterns.
//!
//! Build with: cargo build --release --target wasm32-unknown-unknown
mod memory;
mod types;
use memory::{read_string, write_string};
use types::*;
// Global configuration storage
static mut PLUGIN_CONFIG: Option<PluginConfig> = None;
// =============================================================================
// Exported Plugin Functions
// =============================================================================
/// Return the plugin name
#[no_mangle]
pub extern "C" fn get_name() -> u64 {
write_string("hello-world-wasm-rust")
}
/// Initialize the plugin with config
/// Returns 0 on success, non-zero on error
#[no_mangle]
pub extern "C" fn init(config_ptr: u32, config_len: u32) -> i32 {
let config_str = read_string(config_ptr, config_len);
// Parse configuration
let config: PluginConfig = if config_str.is_empty() {
PluginConfig::default()
} else {
match serde_json::from_str(&config_str) {
Ok(c) => c,
Err(_) => return 1, // Config parse error
}
};
// Store configuration
unsafe {
PLUGIN_CONFIG = Some(config);
}
0 // Success
}
/// HTTP transport intercept
/// Called at the HTTP layer before request enters Bifrost core.
/// Can modify headers, query params, or short-circuit with a response.
#[no_mangle]
pub extern "C" fn http_intercept(input_ptr: u32, input_len: u32) -> u64 {
let input_str = read_string(input_ptr, input_len);
// Parse input
let input: HTTPInterceptInput = match serde_json::from_str(&input_str) {
Ok(i) => i,
Err(e) => {
// Include context around the error position for debugging
let error_context = if let Some(col) = extract_column(&e.to_string()) {
let start = col.saturating_sub(50);
let end = (col + 50).min(input_str.len());
format!(" | context: ...{}...", &input_str[start..end])
} else {
String::new()
};
let output = HTTPInterceptOutput {
error: format!("Failed to parse input: {}{}", e, error_context),
..Default::default()
};
return write_string(&serde_json::to_string(&output).unwrap_or_default());
}
};
// Add context value like Go plugin does
let mut context = input.context;
context.set_value("from-http", serde_json::json!("123"));
// Create output with context and request preserved (pass-through)
// Serialize request to Value to ensure proper JSON structure
let request_value = serde_json::to_value(&input.request).ok();
let output = HTTPInterceptOutput {
context: input.context,
request: input.request,
has_response: false,
..Default::default()
};
// Pass through
write_string(&serde_json::to_string(&output).unwrap_or_default())
}
/// Pre-request hook
/// Called before request is sent to the provider.
/// Can modify the request or short-circuit with a response/error.
#[no_mangle]
pub extern "C" fn pre_hook(input_ptr: u32, input_len: u32) -> u64 {
let input_str = read_string(input_ptr, input_len);
// Parse input
let input: PreHookInput = match serde_json::from_str(&input_str) {
Ok(i) => i,
Err(e) => {
let output = PreHookOutput {
error: format!("Failed to parse input: {}", e),
..Default::default()
};
return write_string(&serde_json::to_string(&output).unwrap_or_default());
}
};
// Create output with context preserved
let mut output = PreHookOutput {
context: input.context.clone(),
request: input.request.clone(),
has_short_circuit: false,
..Default::default()
};
// Get provider and model for potential modifications
let (_provider, model) = input.get_provider_model();
// Example: Short-circuit with mock response for specific model
// Uncomment to test:
/*
if model == "mock-model" {
output.has_short_circuit = true;
let mock_response = BifrostResponse {
chat_response: Some(BifrostChatResponse {
id: format!("mock-{}", input.context.request_id.unwrap_or_default()),
model: "mock-model".to_string(),
choices: vec![ResponseChoice {
index: 0,
message: Some(ChatMessage {
role: ChatMessageRole::Assistant,
content: Some(ChatMessageContent::Text(
"This is a mock response from the Rust WASM plugin!".to_string()
)),
..Default::default()
}),
finish_reason: Some("stop".to_string()),
..Default::default()
}],
usage: Some(LLMUsage {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
output.short_circuit = Some(LLMPluginShortCircuit {
response: Some(mock_response),
error: None,
});
return write_string(&serde_json::to_string(&output).unwrap_or_default());
}
*/
// Example: Short-circuit with rate limit error
// Uncomment to test:
/*
if should_rate_limit(&input.context) {
output.has_short_circuit = true;
output.short_circuit = Some(LLMPluginShortCircuit {
response: None,
error: Some(
BifrostError::new("Rate limit exceeded")
.with_type("rate_limit")
.with_code("429")
.with_status(429)
),
});
return write_string(&serde_json::to_string(&output).unwrap_or_default());
}
*/
// Silence unused variable warning in example code
let _ = model;
// Pass through - empty request means use original
write_string(&serde_json::to_string(&output).unwrap_or_default())
}
/// Post-response hook
/// Called after response is received from provider.
/// Can modify the response or error.
#[no_mangle]
pub extern "C" fn post_hook(input_ptr: u32, input_len: u32) -> u64 {
let input_str = read_string(input_ptr, input_len);
// Parse input
let input: PostHookInput = match serde_json::from_str(&input_str) {
Ok(i) => i,
Err(e) => {
let output = PostHookOutput {
hook_error: format!("Failed to parse input: {}", e),
..Default::default()
};
return write_string(&serde_json::to_string(&output).unwrap_or_default());
}
};
// Add context value like Go plugin does
let mut context = input.context.clone();
context.set_value("from-post-hook", serde_json::json!("456"));
// Create output with context and response/error preserved (pass-through)
// This matches Go plugin behavior exactly
let output = PostHookOutput {
context,
response: Some(input.response.clone()),
error: Some(input.error.clone()),
has_error: input.has_error,
hook_error: String::new(),
};
// Example: Modify error message when has_error is true
// Uncomment to test:
/*
if input.has_error {
if let Some(mut error) = input.parse_error() {
error.error.message = format!("{} (processed by Rust WASM plugin)", error.error.message);
let mut output = output;
output.error = Some(serde_json::to_value(&error).unwrap_or_default());
return write_string(&serde_json::to_string(&output).unwrap_or_default());
}
}
*/
// Example: Modify response
// Uncomment to test:
/*
if let Some(mut response) = input.parse_response() {
// Add custom metadata, modify model name, etc.
if let Some(ref mut chat) = response.chat_response {
// Add a marker to the model name
chat.model = format!("{} (via rust-wasm)", chat.model);
}
let mut output = output;
output.response = Some(serde_json::to_value(&response).unwrap_or_default());
return write_string(&serde_json::to_string(&output).unwrap_or_default());
}
*/
write_string(&serde_json::to_string(&output).unwrap_or_default())
}
/// HTTP stream chunk hook
/// Called for each chunk during streaming responses.
/// Can modify, skip, or stop streaming based on return values.
#[no_mangle]
pub extern "C" fn http_stream_chunk_hook(input_ptr: u32, input_len: u32) -> u64 {
let input_str = read_string(input_ptr, input_len);
// Parse input
let input: HTTPStreamChunkHookInput = match serde_json::from_str(&input_str) {
Ok(i) => i,
Err(e) => {
let output = HTTPStreamChunkHookOutput {
error: format!("Failed to parse input: {}", e),
..Default::default()
};
return write_string(&serde_json::to_string(&output).unwrap_or_default());
}
};
// Add context value like Go plugin does
let mut context = input.context.clone();
context.set_value("from-stream-chunk", serde_json::json!("rust-wasm"));
// Pass through chunk unchanged
let output = HTTPStreamChunkHookOutput {
context,
chunk: Some(input.chunk),
has_chunk: true,
skip: false,
error: String::new(),
};
write_string(&serde_json::to_string(&output).unwrap_or_default())
}
/// Cleanup resources
/// Called when plugin is being unloaded.
/// Returns 0 on success, non-zero on error
#[no_mangle]
pub extern "C" fn cleanup() -> i32 {
// Clear stored configuration
unsafe {
PLUGIN_CONFIG = None;
}
0 // Success
}
// =============================================================================
// Helper Functions
// =============================================================================
/// Extract column number from serde error message for debugging
fn extract_column(error_msg: &str) -> Option<usize> {
// Error format: "... at line X column Y"
if let Some(idx) = error_msg.rfind("column ") {
let col_str = &error_msg[idx + 7..];
col_str.split_whitespace().next()?.parse().ok()
} else {
None
}
}
/// Example rate limit check function
#[allow(dead_code)]
fn should_rate_limit(_context: &BifrostContext) -> bool {
// Implement your rate limiting logic here
false
}

View File

@@ -0,0 +1,70 @@
//! Memory management utilities for WASM plugins.
//! Handles allocation, deallocation, and string read/write operations.
use std::alloc::{alloc, dealloc, Layout};
use std::slice;
/// Pack a pointer and length into a single u64
/// Upper 32 bits: pointer, Lower 32 bits: length
pub fn pack_result(ptr: u32, len: u32) -> u64 {
((ptr as u64) << 32) | (len as u64)
}
/// Write a string to WASM memory and return packed pointer+length
pub fn write_string(s: &str) -> u64 {
if s.is_empty() {
return 0;
}
let bytes = s.as_bytes();
let ptr = unsafe { malloc(bytes.len() as u32) };
if ptr == 0 {
return 0;
}
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr as *mut u8, bytes.len());
}
pack_result(ptr, bytes.len() as u32)
}
/// Read a string from WASM memory given pointer and length
pub fn read_string(ptr: u32, len: u32) -> String {
if len == 0 {
return String::new();
}
let bytes = unsafe { slice::from_raw_parts(ptr as *const u8, len as usize) };
String::from_utf8_lossy(bytes).into_owned()
}
/// Allocate memory for the host to write data
///
/// # Safety
/// This function is marked as safe but performs unsafe operations internally.
/// It is intended to be called from WASM host.
#[no_mangle]
pub extern "C" fn malloc(size: u32) -> u32 {
if size == 0 {
return 0;
}
let layout = match Layout::from_size_align(size as usize, 1) {
Ok(l) => l,
Err(_) => return 0,
};
unsafe { alloc(layout) as u32 }
}
/// Free allocated memory
///
/// # Safety
/// This function is marked as safe but performs unsafe operations internally.
/// It is intended to be called from WASM host.
#[no_mangle]
pub extern "C" fn free(ptr: u32, size: u32) {
if ptr == 0 || size == 0 {
return;
}
let layout = match Layout::from_size_align(size as usize, 1) {
Ok(l) => l,
Err(_) => return,
};
unsafe { dealloc(ptr as *mut u8, layout) }
}

View File

@@ -0,0 +1,870 @@
//! Type definitions for Bifrost WASM plugins.
//! These structures mirror the Go SDK types for interoperability.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
// =============================================================================
// Nullable Deserializers
// =============================================================================
/// Helper module for deserializing fields that may be null in JSON.
/// Go's JSON encoder outputs `null` for nil slices/maps, but Rust's serde
/// with `#[serde(default)]` only handles missing fields, not explicit nulls.
mod nullable {
use serde::{Deserialize, Deserializer};
use std::collections::HashMap;
/// Deserialize a string that may be null, converting null to empty string.
pub fn string<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
Option::<String>::deserialize(deserializer).map(|opt| opt.unwrap_or_default())
}
/// Deserialize a HashMap<String, String> that may be null or contain null values.
/// Handles both `null` (entire map is null) and `{"key": null}` (value is null).
pub fn string_map<'de, D>(deserializer: D) -> Result<HashMap<String, String>, D::Error>
where
D: Deserializer<'de>,
{
// First deserialize as Option<HashMap<String, Option<String>>> to handle null values
let opt_map: Option<HashMap<String, Option<String>>> = Option::deserialize(deserializer)?;
match opt_map {
None => Ok(HashMap::new()),
Some(map) => {
// Filter out null values and unwrap the rest
Ok(map
.into_iter()
.filter_map(|(k, v)| v.map(|val| (k, val)))
.collect())
}
}
}
/// Deserialize an i32 that may be null, converting null to 0.
pub fn i32_field<'de, D>(deserializer: D) -> Result<i32, D::Error>
where
D: Deserializer<'de>,
{
Option::<i32>::deserialize(deserializer).map(|opt| opt.unwrap_or_default())
}
/// Deserialize an HTTPRequest that may be null, converting null to default.
pub fn http_request<'de, D>(deserializer: D) -> Result<super::HTTPRequest, D::Error>
where
D: Deserializer<'de>,
{
Option::<super::HTTPRequest>::deserialize(deserializer).map(|opt| opt.unwrap_or_default())
}
/// Deserialize a BifrostContext that may be null, converting null to default.
pub fn context<'de, D>(deserializer: D) -> Result<super::BifrostContext, D::Error>
where
D: Deserializer<'de>,
{
Option::<super::BifrostContext>::deserialize(deserializer).map(|opt| opt.unwrap_or_default())
}
}
// =============================================================================
// Context Structure
// =============================================================================
/// BifrostContext holds request-scoped values passed between hooks.
/// This is a dynamic map (map[string]any in Go) that can hold any JSON values.
/// Common keys include:
/// - request_id: Unique identifier for the request
/// - Custom plugin values can be added and will be persisted across hooks
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(transparent)]
pub struct BifrostContext(pub HashMap<String, serde_json::Value>);
impl BifrostContext {
pub fn new() -> Self {
Self(HashMap::new())
}
/// Set a custom value in the context
pub fn set_value(&mut self, key: &str, value: impl Into<serde_json::Value>) {
self.0.insert(key.to_string(), value.into());
}
/// Get a value from the context
pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
self.0.get(key)
}
/// Get a string value from the context
pub fn get_string(&self, key: &str) -> Option<&str> {
self.0.get(key).and_then(|v| v.as_str())
}
/// Get a boolean value from the context
pub fn get_bool(&self, key: &str) -> Option<bool> {
self.0.get(key).and_then(|v| v.as_bool())
}
/// Get an i64 value from the context
pub fn get_i64(&self, key: &str) -> Option<i64> {
self.0.get(key).and_then(|v| v.as_i64())
}
/// Check if a key exists in the context
pub fn contains_key(&self, key: &str) -> bool {
self.0.contains_key(key)
}
/// Remove a value from the context
pub fn remove(&mut self, key: &str) -> Option<serde_json::Value> {
self.0.remove(key)
}
/// Get the underlying HashMap for iteration
pub fn inner(&self) -> &HashMap<String, serde_json::Value> {
&self.0
}
/// Get mutable access to the underlying HashMap
pub fn inner_mut(&mut self) -> &mut HashMap<String, serde_json::Value> {
&mut self.0
}
}
// =============================================================================
// HTTP Transport Structures
// =============================================================================
/// HTTPRequest represents an incoming HTTP request at the transport layer.
/// Body is base64-encoded.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct HTTPRequest {
#[serde(default, deserialize_with = "nullable::string")]
pub method: String,
#[serde(default, deserialize_with = "nullable::string")]
pub path: String,
#[serde(default, deserialize_with = "nullable::string_map")]
pub headers: HashMap<String, String>,
#[serde(default, deserialize_with = "nullable::string_map")]
pub query: HashMap<String, String>,
/// Base64-encoded request body
#[serde(default, deserialize_with = "nullable::string")]
pub body: String,
}
/// HTTPResponse represents an HTTP response to return.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct HTTPResponse {
#[serde(default, deserialize_with = "nullable::i32_field")]
pub status_code: i32,
#[serde(default, deserialize_with = "nullable::string_map")]
pub headers: HashMap<String, String>,
/// Base64-encoded response body
#[serde(default, deserialize_with = "nullable::string")]
pub body: String,
}
/// HTTPInterceptInput is the input for http_intercept hook.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct HTTPInterceptInput {
#[serde(default, deserialize_with = "nullable::context")]
pub context: BifrostContext,
#[serde(default, deserialize_with = "nullable::http_request")]
pub request: HTTPRequest,
}
/// HTTPInterceptOutput is the output for http_intercept hook.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct HTTPInterceptOutput {
pub context: BifrostContext,
#[serde(skip_serializing_if = "Option::is_none")]
pub request: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response: Option<HTTPResponse>,
#[serde(default)]
pub has_response: bool,
#[serde(default)]
pub error: String,
}
// =============================================================================
// Chat Completion Structures (BifrostRequest)
// =============================================================================
/// ChatMessageRole represents the role of a message sender.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ChatMessageRole {
User,
Assistant,
System,
Tool,
Developer,
}
impl Default for ChatMessageRole {
fn default() -> Self {
ChatMessageRole::User
}
}
/// ChatMessageContent can be either a string or an array of content blocks.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ChatMessageContent {
Text(String),
Blocks(Vec<ChatContentBlock>),
}
impl Default for ChatMessageContent {
fn default() -> Self {
ChatMessageContent::Text(String::new())
}
}
/// ChatContentBlock represents a content block in a message.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ChatContentBlock {
#[serde(rename = "type")]
pub block_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image_url: Option<ImageUrl>,
}
/// ImageUrl represents an image URL in a content block.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ImageUrl {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
/// ChatMessage represents a message in the conversation.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ChatMessage {
#[serde(default)]
pub role: ChatMessageRole,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<ChatMessageContent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
}
/// ToolCall represents a tool call made by the assistant.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ToolCall {
#[serde(default)]
pub id: Option<String>,
#[serde(rename = "type", default)]
pub call_type: Option<String>,
#[serde(default)]
pub function: ToolCallFunction,
}
/// ToolCallFunction represents the function being called.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ToolCallFunction {
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub arguments: String,
}
/// ChatParameters contains optional parameters for chat completion.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ChatParameters {
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_completion_tokens: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub frequency_penalty: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub presence_penalty: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<ChatTool>>,
/// Catch-all for additional parameters
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
/// ChatTool represents a tool definition.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ChatTool {
#[serde(rename = "type")]
pub tool_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub function: Option<ChatToolFunction>,
}
/// ChatToolFunction represents a function definition.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ChatToolFunction {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<serde_json::Value>,
}
/// BifrostChatRequest represents a chat completion request.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BifrostChatRequest {
#[serde(default)]
pub provider: String,
#[serde(default)]
pub model: String,
#[serde(default)]
pub input: Vec<ChatMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<ChatParameters>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fallbacks: Option<Vec<Fallback>>,
}
/// Fallback represents a fallback provider/model.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Fallback {
pub provider: String,
pub model: String,
}
/// BifrostRequest is the unified request structure.
/// Only one of the request types should be present.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BifrostRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub chat_request: Option<BifrostChatRequest>,
// Add other request types as needed
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
impl BifrostRequest {
/// Get provider and model from the request
pub fn get_provider_model(&self) -> (String, String) {
if let Some(ref chat) = self.chat_request {
return (chat.provider.clone(), chat.model.clone());
}
(String::new(), String::new())
}
}
// =============================================================================
// Response Structures (BifrostResponse)
// =============================================================================
/// LLMUsage contains token usage information.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct LLMUsage {
#[serde(default)]
pub prompt_tokens: i32,
#[serde(default)]
pub completion_tokens: i32,
#[serde(default)]
pub total_tokens: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_tokens_details: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completion_tokens_details: Option<serde_json::Value>,
}
/// ResponseChoice represents a single completion choice.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ResponseChoice {
#[serde(default)]
pub index: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<ChatMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub delta: Option<ChatMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub finish_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logprobs: Option<serde_json::Value>,
}
/// BifrostChatResponse represents a chat completion response.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BifrostChatResponse {
#[serde(default)]
pub id: String,
#[serde(default)]
pub model: String,
#[serde(default)]
pub choices: Vec<ResponseChoice>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<LLMUsage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub object: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system_fingerprint: Option<String>,
}
/// BifrostResponse is the unified response structure.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BifrostResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub chat_response: Option<BifrostChatResponse>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
// =============================================================================
// Error Structure
// =============================================================================
/// ErrorField contains the error details.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ErrorField {
#[serde(default)]
pub message: String,
#[serde(skip_serializing_if = "Option::is_none", rename = "type")]
pub error_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub param: Option<String>,
}
/// BifrostError represents an error response.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BifrostError {
#[serde(default)]
pub error: ErrorField,
#[serde(skip_serializing_if = "Option::is_none")]
pub status_code: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_fallbacks: Option<bool>,
}
impl BifrostError {
/// Create a new error with a message
pub fn new(message: &str) -> Self {
Self {
error: ErrorField {
message: message.to_string(),
..Default::default()
},
..Default::default()
}
}
/// Set the error type
pub fn with_type(mut self, error_type: &str) -> Self {
self.error.error_type = Some(error_type.to_string());
self
}
/// Set the error code
pub fn with_code(mut self, code: &str) -> Self {
self.error.code = Some(code.to_string());
self
}
/// Set the status code
pub fn with_status(mut self, status: i32) -> Self {
self.status_code = Some(status);
self
}
}
// =============================================================================
// Short Circuit Structure
// =============================================================================
/// LLMPluginShortCircuit allows plugins to short-circuit the request flow.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct LLMPluginShortCircuit {
#[serde(skip_serializing_if = "Option::is_none")]
pub response: Option<BifrostResponse>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<BifrostError>,
}
// =============================================================================
// Hook Input/Output Structures
// =============================================================================
/// PreHookInput is the input for pre_hook.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PreHookInput {
#[serde(default)]
pub context: BifrostContext,
#[serde(default)]
pub request: serde_json::Value,
}
impl PreHookInput {
/// Parse the request as a BifrostRequest
pub fn parse_request(&self) -> Option<BifrostRequest> {
serde_json::from_value(self.request.clone()).ok()
}
/// Get provider and model from the request
pub fn get_provider_model(&self) -> (String, String) {
if let Some(req) = self.parse_request() {
return req.get_provider_model();
}
// Try direct access for simpler structures
let provider = self.request.get("provider")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let model = self.request.get("model")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
(provider, model)
}
}
/// PreHookOutput is the output for pre_hook.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PreHookOutput {
pub context: BifrostContext,
#[serde(skip_serializing_if = "Option::is_none")]
pub request: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub short_circuit: Option<LLMPluginShortCircuit>,
#[serde(default)]
pub has_short_circuit: bool,
#[serde(default)]
pub error: String,
}
/// PostHookInput is the input for post_hook.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PostHookInput {
#[serde(default)]
pub context: BifrostContext,
#[serde(default)]
pub response: serde_json::Value,
#[serde(default)]
pub error: serde_json::Value,
#[serde(default)]
pub has_error: bool,
}
impl PostHookInput {
/// Parse the response as a BifrostResponse
pub fn parse_response(&self) -> Option<BifrostResponse> {
serde_json::from_value(self.response.clone()).ok()
}
/// Parse the error as a BifrostError
pub fn parse_error(&self) -> Option<BifrostError> {
if self.has_error {
serde_json::from_value(self.error.clone()).ok()
} else {
None
}
}
}
/// PostHookOutput is the output for post_hook.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PostHookOutput {
pub context: BifrostContext,
#[serde(skip_serializing_if = "Option::is_none")]
pub response: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<serde_json::Value>,
#[serde(default)]
pub has_error: bool,
#[serde(default)]
pub hook_error: String,
}
// =============================================================================
// HTTP Stream Chunk Hook Input/Output Structures
// =============================================================================
/// HTTPStreamChunkHookInput is the input for http_stream_chunk_hook.
/// Called for each chunk during streaming responses.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct HTTPStreamChunkHookInput {
#[serde(default)]
pub context: BifrostContext,
#[serde(default)]
pub request: serde_json::Value,
#[serde(default)]
pub chunk: serde_json::Value, // BifrostStreamChunk as JSON
}
/// HTTPStreamChunkHookOutput is the output for http_stream_chunk_hook.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct HTTPStreamChunkHookOutput {
pub context: BifrostContext,
#[serde(skip_serializing_if = "Option::is_none")]
pub chunk: Option<serde_json::Value>, // BifrostStreamChunk as JSON, None to skip
#[serde(default)]
pub has_chunk: bool,
#[serde(default)]
pub skip: bool,
#[serde(default)]
pub error: String,
}
// =============================================================================
// Plugin Configuration
// =============================================================================
/// Plugin configuration (customize as needed)
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PluginConfig {
#[serde(flatten)]
pub values: HashMap<String, serde_json::Value>,
}
// =============================================================================
// Tests
// =============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_context_serialization() {
let mut ctx = BifrostContext::new();
ctx.set_value("request_id", "test-123");
ctx.set_value("custom_key", "custom_value");
ctx.set_value("is_enabled", true);
ctx.set_value("count", 42);
let json = serde_json::to_string(&ctx).unwrap();
assert!(json.contains("request_id"));
assert!(json.contains("custom_key"));
assert!(json.contains("is_enabled"));
assert!(json.contains("count"));
}
#[test]
fn test_context_deserialization() {
let json = r#"{"request_id": "test-123", "custom_key": "custom_value", "is_enabled": true}"#;
let ctx: BifrostContext = serde_json::from_str(json).unwrap();
assert_eq!(ctx.get_string("request_id"), Some("test-123"));
assert_eq!(ctx.get_string("custom_key"), Some("custom_value"));
assert_eq!(ctx.get_bool("is_enabled"), Some(true));
}
#[test]
fn test_context_methods() {
let mut ctx = BifrostContext::new();
ctx.set_value("key1", "value1");
ctx.set_value("enabled", true);
ctx.set_value("count", 42);
assert_eq!(ctx.get_string("key1"), Some("value1"));
assert_eq!(ctx.get_bool("enabled"), Some(true));
assert_eq!(ctx.get_i64("count"), Some(42));
assert!(ctx.contains_key("key1"));
assert!(!ctx.contains_key("nonexistent"));
ctx.remove("key1");
assert!(!ctx.contains_key("key1"));
}
#[test]
fn test_chat_message() {
let msg = ChatMessage {
role: ChatMessageRole::User,
content: Some(ChatMessageContent::Text("Hello!".to_string())),
..Default::default()
};
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("user"));
assert!(json.contains("Hello!"));
}
#[test]
fn test_bifrost_error() {
let error = BifrostError::new("Test error")
.with_type("test_type")
.with_code("500")
.with_status(500);
let json = serde_json::to_string(&error).unwrap();
assert!(json.contains("Test error"));
assert!(json.contains("test_type"));
}
#[test]
fn test_pre_hook_input_parsing() {
let json = r#"{
"context": {"request_id": "test-123", "custom": "value"},
"request": {"provider": "openai", "model": "gpt-4"}
}"#;
let input: PreHookInput = serde_json::from_str(json).unwrap();
assert_eq!(input.context.get_string("request_id"), Some("test-123"));
assert_eq!(input.context.get_string("custom"), Some("value"));
let (provider, model) = input.get_provider_model();
assert_eq!(provider, "openai");
assert_eq!(model, "gpt-4");
}
#[test]
fn test_http_request_with_null_fields() {
// Simulates Go sending null for nil []byte and nil maps
let json = r#"{
"method": "POST",
"path": "/v1/chat/completions",
"headers": null,
"query": null,
"body": null
}"#;
let req: HTTPRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.method, "POST");
assert_eq!(req.path, "/v1/chat/completions");
assert!(req.headers.is_empty());
assert!(req.query.is_empty());
assert_eq!(req.body, "");
}
#[test]
fn test_http_request_with_missing_fields() {
// Test that missing fields also work (default behavior)
let json = r#"{
"method": "GET",
"path": "/health"
}"#;
let req: HTTPRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.method, "GET");
assert_eq!(req.path, "/health");
assert!(req.headers.is_empty());
assert!(req.query.is_empty());
assert_eq!(req.body, "");
}
#[test]
fn test_http_intercept_input_with_nulls() {
// Simulates a full HTTP intercept input with null body from Go
let json = r#"{
"context": {"request_id": "abc-123"},
"request": {
"method": "POST",
"path": "/v1/chat/completions",
"headers": {"content-type": "application/json"},
"query": {},
"body": null
}
}"#;
let input: HTTPInterceptInput = serde_json::from_str(json).unwrap();
assert_eq!(input.context.get_string("request_id"), Some("abc-123"));
assert_eq!(input.request.method, "POST");
assert_eq!(input.request.path, "/v1/chat/completions");
assert_eq!(input.request.headers.get("content-type"), Some(&"application/json".to_string()));
assert_eq!(input.request.body, "");
}
#[test]
fn test_http_response_with_null_fields() {
let json = r#"{
"status_code": null,
"headers": null,
"body": null
}"#;
let resp: HTTPResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.status_code, 0);
assert!(resp.headers.is_empty());
assert_eq!(resp.body, "");
}
}

View File

@@ -0,0 +1,70 @@
.PHONY: all build build-debug clean help install check-node
# Colors
COLOR_RESET = \033[0m
COLOR_INFO = \033[36m
COLOR_SUCCESS = \033[32m
COLOR_WARNING = \033[33m
COLOR_ERROR = \033[31m
COLOR_BOLD = \033[1m
# Plugin configuration
PLUGIN_NAME = hello-world
OUTPUT_DIR = build
OUTPUT = $(OUTPUT_DIR)/$(PLUGIN_NAME).wasm
help: ## Show this help message
@echo '$(COLOR_BOLD)Hello World WASM Plugin (TypeScript/AssemblyScript)$(COLOR_RESET)'
@echo ''
@echo '$(COLOR_BOLD)Usage:$(COLOR_RESET) make [target]'
@echo ''
@echo '$(COLOR_BOLD)Prerequisites:$(COLOR_RESET)'
@echo ' - Node.js (https://nodejs.org/)'
@echo ' - npm install (to install AssemblyScript)'
@echo ''
@echo '$(COLOR_BOLD)Available targets:$(COLOR_RESET)'
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " $(COLOR_INFO)%-15s$(COLOR_RESET) %s\n", $$1, $$2}' $(MAKEFILE_LIST)
check-node: ## Check if Node.js is installed
@which node > /dev/null 2>&1 || (echo "$(COLOR_ERROR)Error: Node.js is not installed$(COLOR_RESET)"; \
echo "$(COLOR_INFO)Install Node.js: https://nodejs.org/$(COLOR_RESET)"; \
exit 1)
@echo "$(COLOR_SUCCESS)✓ Node.js found: $$(node --version)$(COLOR_RESET)"
install: check-node ## Install dependencies
@echo "$(COLOR_INFO)Installing dependencies...$(COLOR_RESET)"
npm install
@echo "$(COLOR_SUCCESS)✓ Dependencies installed$(COLOR_RESET)"
build: install ## Build the WASM plugin
@mkdir -p $(OUTPUT_DIR)
@echo "$(COLOR_INFO)Building WASM plugin...$(COLOR_RESET)"
npm run build
@echo "$(COLOR_SUCCESS)✓ Plugin built successfully: $(OUTPUT)$(COLOR_RESET)"
@ls -lh $(OUTPUT) | awk '{print " Size: " $$5}'
build-debug: install ## Build with debug info
@mkdir -p $(OUTPUT_DIR)
@echo "$(COLOR_INFO)Building WASM plugin (debug)...$(COLOR_RESET)"
npm run build:debug
@echo "$(COLOR_SUCCESS)✓ Debug plugin built: $(OUTPUT)$(COLOR_RESET)"
@ls -lh $(OUTPUT) | awk '{print " Size: " $$5}'
clean: ## Remove build artifacts
@echo "$(COLOR_INFO)Cleaning build artifacts...$(COLOR_RESET)"
@rm -rf $(OUTPUT_DIR) node_modules
@echo "$(COLOR_SUCCESS)✓ Clean complete$(COLOR_RESET)"
info: ## Show build information
@echo "$(COLOR_BOLD)Build Configuration$(COLOR_RESET)"
@echo " Plugin Name: $(PLUGIN_NAME)"
@echo " Output: $(OUTPUT)"
@echo ""
@if [ -f "$(OUTPUT)" ]; then \
echo "$(COLOR_SUCCESS)Plugin exists:$(COLOR_RESET)"; \
ls -lh $(OUTPUT) | awk '{print " " $$9 " (" $$5 ")"}'; \
else \
echo "$(COLOR_WARNING)Plugin not built yet$(COLOR_RESET)"; \
fi
.DEFAULT_GOAL := help

View File

@@ -0,0 +1,453 @@
# Bifrost WASM Plugin (TypeScript/AssemblyScript)
A comprehensive example of a Bifrost plugin written in TypeScript and compiled to WebAssembly using AssemblyScript. This plugin demonstrates proper structure definitions, JSON parsing, context handling, and request/response modification patterns.
## Prerequisites
### Node.js Installation
Node.js is required to run AssemblyScript:
**macOS:**
```bash
brew install node
```
**Linux:**
```bash
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
```
**Other platforms:**
See [Node.js Downloads](https://nodejs.org/en/download/)
## Building
```bash
# Install dependencies and build
make build
# Build with debug info
make build-debug
# Clean build artifacts
make clean
```
The compiled plugin will be at `build/hello-world.wasm`.
## File Structure
```
assembly/
├── index.ts # Plugin implementation (hooks)
├── memory.ts # Memory management utilities
├── types.ts # Type definitions (mirrors Go SDK)
└── tsconfig.json # AssemblyScript config
```
## Plugin Structure
WASM plugins must export the following functions:
| Export | Signature | Description |
|--------|-----------|-------------|
| `malloc` | `(size: u32) -> u32` | Allocate memory for host to write data |
| `free` | `(ptr: u32)` | Free allocated memory |
| `get_name` | `() -> u64` | Returns packed ptr+len of plugin name |
| `init` | `(config_ptr, config_len: u32) -> i32` | Initialize with config (optional) |
| `http_intercept` | `(input_ptr, input_len: u32) -> u64` | HTTP transport intercept |
| `pre_hook` | `(input_ptr, input_len: u32) -> u64` | Pre-request hook |
| `post_hook` | `(input_ptr, input_len: u32) -> u64` | Post-response hook |
| `cleanup` | `() -> i32` | Cleanup resources (0 = success) |
### Return Value Format
Functions returning data use a packed `u64` format:
- Upper 32 bits: pointer to data in WASM memory
- Lower 32 bits: length of data
## Data Structures
This plugin uses `json-as` with `@json` decorators for automatic JSON serialization. All structures mirror the Go SDK types:
### Context
```typescript
@json
class BifrostContext {
request_id: string = '' // Unique request identifier
plugin_processed: string = '' // Custom plugin values
plugin_name: string = ''
}
```
### HTTP Transport Types
```typescript
@json
class HTTPRequest {
method: string = '' // GET, POST, etc.
path: string = '' // /v1/chat/completions
body: string = '' // base64 encoded
}
@json
class HTTPResponse {
status_code: i32 = 200 // HTTP status code
body: string = '' // base64 encoded
}
```
### Chat Completion Types
```typescript
@json
class ChatMessage {
role: string = '' // "user", "assistant", "system", "tool"
content: string = ''
name: string = ''
tool_call_id: string = ''
}
@json
class ChatParameters {
temperature: f64 = 0
max_completion_tokens: i32 = 0
top_p: f64 = 0
}
@json
class BifrostChatRequest {
provider: string = '' // "openai", "anthropic", etc.
model: string = '' // "gpt-4", "claude-3", etc.
input: ChatMessage[] = []
params: ChatParameters = new ChatParameters()
}
```
### Response Types
```typescript
@json
class LLMUsage {
prompt_tokens: i32 = 0
completion_tokens: i32 = 0
total_tokens: i32 = 0
}
@json
class ResponseChoice {
index: i32 = 0
message: ChatMessage = new ChatMessage()
finish_reason: string = 'stop' // "stop", "length", "tool_calls"
}
@json
class BifrostChatResponse {
id: string = ''
model: string = ''
choices: ResponseChoice[] = []
usage: LLMUsage = new LLMUsage()
}
```
### Error Types
```typescript
@json
class ErrorField {
message: string = ''
type: string = '' // "rate_limit", "auth_error", etc.
code: string = '' // "429", "401", etc.
}
@json
class BifrostError {
error: ErrorField = new ErrorField()
status_code: i32 = 0
}
```
### Short Circuit
```typescript
@json
class LLMPluginShortCircuit {
response: BifrostResponse | null = null // Success short-circuit
error: BifrostError | null = null // Error short-circuit
}
```
## Hook Input/Output Structures
### http_intercept
**Input:**
```json
{
"context": { "request_id": "abc-123" },
"request": {
"method": "POST",
"path": "/v1/chat/completions",
"headers": { "Content-Type": "application/json" },
"query": {},
"body": "<base64-encoded>"
}
}
```
**Output:**
```json
{
"context": { "request_id": "abc-123", "custom_key": "value" },
"request": {},
"response": { "status_code": 200, "headers": {}, "body": "<base64>" },
"has_response": false,
"error": ""
}
```
### pre_hook
**Input:**
```json
{
"context": { "request_id": "abc-123" },
"request": {
"provider": "openai",
"model": "gpt-4",
"input": [{ "role": "user", "content": "Hello" }],
"params": { "temperature": 0.7 }
}
}
```
**Output:**
```json
{
"context": { "request_id": "abc-123", "plugin_processed": "true" },
"request": {},
"short_circuit": {
"response": { "chat_response": { ... } }
},
"has_short_circuit": false,
"error": ""
}
```
### post_hook
**Input:**
```json
{
"context": { "request_id": "abc-123", "plugin_processed": "true" },
"response": {
"chat_response": {
"id": "chatcmpl-123",
"model": "gpt-4",
"choices": [{ "index": 0, "message": { "role": "assistant", "content": "Hi!" } }],
"usage": { "prompt_tokens": 5, "completion_tokens": 10, "total_tokens": 15 }
}
},
"error": {},
"has_error": false
}
```
**Output:**
```json
{
"context": { "request_id": "abc-123", "post_hook_completed": "true" },
"response": {},
"error": {},
"has_error": false,
"hook_error": ""
}
```
## Usage Examples
### Modifying Context
```typescript
import { JSON } from 'json-as'
export function pre_hook(inputPtr: u32, inputLen: u32): u64 {
const inputJson = readString(inputPtr, inputLen)
const input = JSON.parse<PreHookInput>(inputJson)
const output = new PreHookOutput()
output.context = input.context
// Add custom values to context
output.context.plugin_processed = 'true'
output.context.plugin_name = 'my-plugin'
return writeString(JSON.stringify(output))
}
```
### Short-Circuit with Mock Response
```typescript
import { JSON } from 'json-as'
export function pre_hook(inputPtr: u32, inputLen: u32): u64 {
const inputJson = readString(inputPtr, inputLen)
const input = JSON.parse<PreHookInput>(inputJson)
// Check if this should be mocked
const model = input.request.model
if (model === 'mock-model') {
const output = new PreHookOutput()
output.context = input.context
output.has_short_circuit = true
output.short_circuit = new LLMPluginShortCircuit()
// Build mock response
const mockResponse = new BifrostResponse()
mockResponse.chat_response = new BifrostChatResponse()
mockResponse.chat_response!.id = 'mock-' + input.context.request_id
mockResponse.chat_response!.model = 'mock-model'
const choice = new ResponseChoice()
choice.message.role = 'assistant'
choice.message.content = 'This is a mock response!'
mockResponse.chat_response!.choices.push(choice)
mockResponse.chat_response!.usage.prompt_tokens = 10
mockResponse.chat_response!.usage.completion_tokens = 15
mockResponse.chat_response!.usage.total_tokens = 25
output.short_circuit!.response = mockResponse
return writeString(JSON.stringify(output))
}
// Pass through
const output = new PreHookOutput()
output.context = input.context
return writeString(JSON.stringify(output))
}
```
### Short-Circuit with Error
```typescript
import { JSON } from 'json-as'
export function pre_hook(inputPtr: u32, inputLen: u32): u64 {
const inputJson = readString(inputPtr, inputLen)
const input = JSON.parse<PreHookInput>(inputJson)
// Check rate limit (example)
if (shouldRateLimit(input.context.request_id)) {
const output = new PreHookOutput()
output.context = input.context
output.has_short_circuit = true
output.short_circuit = new LLMPluginShortCircuit()
const error = new BifrostError()
error.error.message = 'Rate limit exceeded'
error.error.type = 'rate_limit'
error.error.code = '429'
error.status_code = 429
output.short_circuit!.error = error
return writeString(JSON.stringify(output))
}
// Pass through
const output = new PreHookOutput()
output.context = input.context
return writeString(JSON.stringify(output))
}
```
### Modifying Responses in post_hook
```typescript
import { JSON } from 'json-as'
export function post_hook(inputPtr: u32, inputLen: u32): u64 {
const inputJson = readString(inputPtr, inputLen)
const input = JSON.parse<PostHookInput>(inputJson)
const output = new PostHookOutput()
output.context = input.context
// Handle errors
if (input.has_error && input.error !== null) {
output.has_error = true
output.error = input.error
// Could modify error here if needed
return writeString(JSON.stringify(output))
}
// Modify response
if (input.response !== null && input.response!.chat_response !== null) {
output.response = input.response
// Could add logging, metrics, or modify response here
}
return writeString(JSON.stringify(output))
}
```
## Usage with Bifrost
Configure the plugin in your Bifrost config:
```json
{
"plugins": [
{
"path": "/path/to/hello-world.wasm",
"name": "hello-world-wasm-typescript",
"enabled": true,
"config": {
"custom_option": "value"
}
}
]
}
```
## AssemblyScript Notes
AssemblyScript is similar to TypeScript but with some differences:
1. **Types are required**: All variables must have explicit types
2. **No closures**: Functions cannot capture variables from outer scope
3. **Limited stdlib**: Not all JavaScript/TypeScript features are available
4. **Strict null handling**: Null checks are required
5. **JSON via json-as**: Uses the `json-as` package with `@json` decorators for serialization
This plugin uses `json-as` for JSON parsing/serialization:
```typescript
import { JSON } from 'json-as'
@json
class MyClass {
name: string = ''
value: i32 = 0
}
// Parse JSON
const obj = JSON.parse<MyClass>('{"name":"test","value":42}')
// Stringify to JSON
const json = JSON.stringify(obj)
```
See [AssemblyScript Documentation](https://www.assemblyscript.org/introduction.html) and [json-as Documentation](https://github.com/JairusSW/as-json) for more details.
## Benefits
1. **Familiar syntax**: TypeScript-like syntax for JS/TS developers
2. **Cross-platform**: Single `.wasm` binary runs on any OS/architecture
3. **Security**: WASM provides sandboxed execution
4. **Type Safety**: Strongly typed structures catch errors at compile time
5. **npm ecosystem**: Can use npm for dependency management

View File

@@ -0,0 +1,128 @@
/**
* Bifrost WASM Plugin for TypeScript/AssemblyScript
*
* This plugin uses json-as for safe JSON parsing with @json decorators.
*
* Build with: npm run build
*/
import { JSON } from 'json-as'
import { free as _free, malloc as _malloc, readString, writeString } from './memory'
import {
HTTPInterceptInput,
HTTPInterceptOutput,
PreHookInput,
PreHookOutput,
PostHookInput,
PostHookOutput,
HTTPStreamChunkHookInput,
HTTPStreamChunkHookOutput
} from './types'
// =============================================================================
// Re-export memory functions for WASM host
// =============================================================================
export function malloc(size: u32): u32 {
return _malloc(size)
}
export function free(ptr: u32): void {
_free(ptr)
}
// =============================================================================
// Plugin Configuration
// =============================================================================
let pluginConfig: string = ''
// =============================================================================
// Exported Plugin Functions
// =============================================================================
export function get_name(): u64 {
return writeString('hello-world-wasm-typescript')
}
export function init(configPtr: u32, configLen: u32): i32 {
pluginConfig = readString(configPtr, configLen)
return 0
}
/**
* HTTP transport intercept
* Pass through the request with added context value
*/
export function http_intercept(inputPtr: u32, inputLen: u32): u64 {
const inputJson = readString(inputPtr, inputLen)
const input = JSON.parse<HTTPInterceptInput>(inputJson)
const output = new HTTPInterceptOutput()
output.context = input.context
output.context.set<string>('from-http', '123')
output.request = input.request
return writeString(JSON.stringify(output))
}
/**
* Pre-request hook
* Pass through the request with added context value
*/
export function pre_hook(inputPtr: u32, inputLen: u32): u64 {
const inputJson = readString(inputPtr, inputLen)
const input = JSON.parse<PreHookInput>(inputJson)
const output = new PreHookOutput()
output.context = input.context
output.context.set<string>('from-pre-hook', '789')
output.request = input.request
return writeString(JSON.stringify(output))
}
/**
* Post-response hook
* Pass through the response/error with added context value
*/
export function post_hook(inputPtr: u32, inputLen: u32): u64 {
const inputJson = readString(inputPtr, inputLen)
const input = JSON.parse<PostHookInput>(inputJson)
const output = new PostHookOutput()
output.context = input.context
output.context.set<string>('from-post-hook', '456')
output.response = input.response
output.error = input.error
output.has_error = input.has_error
return writeString(JSON.stringify(output))
}
/**
* HTTP stream chunk hook
* Called for each chunk during streaming responses.
* Pass through the chunk with added context value.
*/
export function http_stream_chunk_hook(inputPtr: u32, inputLen: u32): u64 {
const inputJson = readString(inputPtr, inputLen)
const input = JSON.parse<HTTPStreamChunkHookInput>(inputJson)
const output = new HTTPStreamChunkHookOutput()
output.context = input.context
output.context.set<string>('from-stream-chunk', 'wasm-plugin')
output.chunk = input.chunk
output.has_chunk = true
output.skip = false
return writeString(JSON.stringify(output))
}
/**
* Cleanup resources
*/
export function cleanup(): i32 {
pluginConfig = ''
return 0
}

View File

@@ -0,0 +1,45 @@
/**
* Memory management utilities for WASM plugins.
* Handles allocation, deallocation, and string read/write operations.
*/
// Pack a pointer and length into a single u64
// Upper 32 bits: pointer, Lower 32 bits: length
export function packResult(ptr: u32, len: u32): u64 {
return (u64(ptr) << 32) | u64(len)
}
// Write a string to memory and return packed pointer+length
export function writeString(s: string): u64 {
if (s.length === 0) {
return 0
}
const encoded = String.UTF8.encode(s)
const ptr = changetype<u32>(encoded)
return packResult(ptr, encoded.byteLength)
}
// Read a string from memory given pointer and length
export function readString(ptr: u32, len: u32): string {
if (len === 0) {
return ''
}
const buffer = new ArrayBuffer(len)
memory.copy(changetype<usize>(buffer), ptr, len)
return String.UTF8.decode(buffer)
}
// Allocate memory for the host to write data
export function malloc(size: u32): u32 {
if (size === 0) {
return 0
}
const buffer = new ArrayBuffer(size)
return changetype<u32>(buffer)
}
// Free allocated memory (handled by AssemblyScript runtime)
export function free(_ptr: u32): void {
// AssemblyScript handles garbage collection
// This is provided for API compatibility
}

Some files were not shown because too many files have changed in this diff Show More