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