@future-fantasy/mcp-live-bridge
v0.1.14
Published
Config-driven CLI that exposes external HTTP APIs as MCP tools
Readme
mcp-live-bridge
Config-driven CLI that exposes external HTTP APIs as MCP tools.
mcp-live-bridge reads a configuration file describing HTTP endpoints and automatically exposes them as Model Context Protocol (MCP) tools via Streamable HTTP transport. No per-API coding required — define your endpoints in YAML/JSON/TOML and they become callable MCP tools.
Features
- Zero-code tool creation — Define HTTP endpoints in config, get MCP tools instantly
- Flexible auth system — Built-in Form and OAuth2 providers, plus custom provider support
- Template engine — Handlebars-based parameter mapping for URL, query, body, and headers
- Response transformation — JSONPath extraction and Handlebars template formatting
- Multi-format config — YAML, JSON, or TOML
- Auth lifecycle management — Auto token refresh, polling validation, and retry on 401
- Per-session transport — Each MCP client gets its own transport instance, supporting multiple simultaneous connections
- Debug logging — Configurable log levels (
quiet,default,verbose,debug) with request/response detail tracing - OpenAPI import — Generate bridge config from OpenAPI/Swagger specs
- Interactive init — Guided config generation wizard
- Streamable HTTP transport — Uses the official MCP SDK with Streamable HTTP
- CLI interface —
start,validate,list,init, andimportcommands
Quick Start
Install
npm install -g mcp-live-bridgeOr build from source:
git clone https://github.com/<your-org>/mcp-live-bridge.git
cd mcp-live-bridge
npm install
npm run buildCreate a config file
# bridge-config.yaml
name: my-api-bridge
server:
port: 8090
auth:
provider: form
config:
login_url: https://api.example.com/login
username: your-username
password: your-password
tools:
- name: list_users
description: "List all users"
url: https://api.example.com/users
method: GET
- name: get_user
description: "Get a user by ID"
url: https://api.example.com/users/{{params.id}}
method: GET
parameters:
id:
type: integer
required: true
description: "User ID"
location: pathStart the server
mcp-live-bridge start -c bridge-config.yamlThe MCP server is now listening on http://localhost:8090.
Configure with Claude Desktop / IDE
Add to your MCP client config (e.g., Claude Desktop claude_desktop_config.json):
{
"mcpServers": {
"my-api-bridge": {
"url": "http://localhost:8090/mcp",
"headers": {}
}
}
}CLI Commands
# Start the MCP server
mcp-live-bridge start -c <config-file> [options]
Options:
-p, --port <number> Override server port
--verbose Verbose logging (can also be set via server.log_level in config)
--quiet Errors only
# Validate config file without starting
mcp-live-bridge validate -c <config-file>
# List all tools defined in config
mcp-live-bridge list -c <config-file>
# Interactive config generation wizard
mcp-live-bridge init
# Specify output file
mcp-live-bridge init -o my-config.yaml
# Import from OpenAPI spec
mcp-live-bridge import -u https://api.example.com/openapi.json -n my-bridge
# Import from local file
mcp-live-bridge import -f ./openapi.yaml -n my-bridge -o config.yamlConfiguration Reference
Full Config Schema
name: bridge-name # Required: Bridge instance name
version: "1.0" # Optional: Config format version (default: "1.0")
server: # Optional: Server settings
host: 0.0.0.0 # Default: 0.0.0.0
port: 8080 # Default: 8080
cors_origin: "*" # Default: "*"
cors_allow_headers: # Default: Content-Type, Authorization, MCP-Session-Id, Mcp-Protocol-Version
- Content-Type
- Authorization
- MCP-Session-Id
- Mcp-Protocol-Version
cors_allow_methods: # Default: GET, POST, DELETE, OPTIONS
- GET
- POST
- DELETE
- OPTIONS
cors_expose_headers: # Default: MCP-Session-Id, Mcp-Protocol-Version
- MCP-Session-Id
- Mcp-Protocol-Version
timeout: 30000 # Request timeout in ms (default: 30000)
log_level: default # "quiet" | "default" | "verbose" | "debug" (default: "default")
auth: # Required: Authentication config
provider: form # Built-in: "form" | "oauth2" | path to custom .mjs
config: {} # Provider-specific config
validation: {} # Optional: Auth validation
refresh: {} # Optional: Refresh strategy
headers: # Optional: Global default headers
Content-Type: application/json
tools: # Required: Array of tool definitions
- name: tool_name # Required: Tool name (unique)
description: "Tool description" # Required: Tool description
url: https://api.example.com # Required: Full endpoint URL
method: GET # Required: HTTP method
headers: {} # Optional: Tool-level headers (merged with global)
body: "" # Optional: Request body template (string or object)
content_type: "" # Optional: Content-Type override
parameters: {} # Optional: Parameter definitions
response: {} # Optional: Response transformationTool Parameters
Each parameter supports these fields:
| Field | Type | Required | Description |
|-------------|----------|----------|----------------------------------|
| type | string | Yes | string, number, integer, boolean, array, object |
| required | boolean | No | Whether the parameter is required (default: inferred from default) |
| default | any | No | Default value |
| description | string | No | Parameter description |
| location | string | Yes | path, query, body, header |
| enum | string[] | No | Allowed values |
Parameters are injected into templates via {{params.<name>}}.
Request Body
The body field supports two formats:
String template (original, still supported):
body: '{"username": "{{params.username}}", "email": "{{params.email}}"}'Object definition (recommended):
body:
username: "{{params.username}}"
email: "{{params.email}}"
role: admin # Static values are preserved as-is
count: "{{params.n}}" # Rendered as stringThe object is serialized based on content_type:
| content_type | Serialization |
|---|---|
| application/json (default) | JSON.stringify |
| application/x-www-form-urlencoded | key=value&key2=value2 |
URL-encoded example:
content_type: application/x-www-form-urlencoded
body:
username: "{{params.username}}"
password: "{{params.password}}"Nested objects are supported for JSON and automatically flattened for URL-encoded:
body:
user:
name: "{{params.name}}"
email: "{{params.email}}"Response Transformation
tools:
- name: search
url: https://api.example.com/search?q={{params.query}}
method: GET
response:
extract: "$.results[*]" # JSONPath expression to extract data
template: "{{#each this}}{{name}}: {{url}}\n{{/each}}" # Handlebars template- extract: JSONPath expression to extract a subset of the response
- template: Handlebars template to format the extracted data
If omitted, the raw JSON response is returned.
Custom Handler Tools
For scenarios where a single tool needs to call multiple HTTP endpoints, you can use type: handler to define a custom handler script:
tools:
# Standard HTTP tool (default)
- name: get_user
url: https://api.example.com/users/{{params.id}}
method: GET
# Custom handler tool
- name: create_user_and_profile
type: handler
handler: ./tools/create-user-and-profile.mjs
description: "Create a user and fetch their profile"
parameters:
username:
type: string
required: true
description: "Username for the new account"
location: handler
email:
type: string
required: true
description: "Email for the new account"
location: handler
password:
type: string
required: true
description: "Password for the new account"
location: handlerThe handler is an ESM module with a default export function. It receives params and a context object:
// tools/create-user-and-profile.mjs
export default async function(params, ctx) {
const { http, auth, logger } = ctx;
// Step 1: Create user
const user = await http.post('https://api.example.com/users', {
headers: auth,
body: { username: params.username, email: params.email, password: params.password },
});
// Step 2: Fetch profile using the returned ID
const profile = await http.get(`https://api.example.com/users/${user.id}/profile`, {
headers: auth,
});
return { user, profile };
}Available context properties:
| Property | Description |
|---|---|
| http.get(url, opts?) | GET request, auto-parses JSON response |
| http.post(url, opts?) | POST request; body is converted to string via String(), auto-parses JSON response |
| http.put(url, opts?) | PUT request; body is converted to string via String(), auto-parses JSON response |
| http.delete(url, opts?) | DELETE request, auto-parses JSON response |
| http.request(req) | Raw request (returns { status, body, headers }) |
| auth | Current auth headers (e.g., { Authorization: "Bearer ..." }) |
| config | auth.config values from config file |
| logger | Logger instance (info, debug, warn, error) |
All http.* methods accept an options object: { headers?, body?, params? }. The params field appends query parameters to the URL. The body field accepts any type and is converted to string via String() before sending. The Content-Type header is not set automatically — you must set it yourself via headers and serialize the body appropriately.
More examples:
// Form-encoded POST
export default async function(params, ctx) {
const result = await ctx.http.post('https://api.example.com/login', {
headers: { 'Content-Type': 'application/x-www-form-urlencoded', ...ctx.auth },
body: `username=${encodeURIComponent(params.username)}&password=${encodeURIComponent(params.password)}`,
});
return result;
}// JSON POST (explicit serialization and Content-Type)
export default async function(params, ctx) {
const result = await ctx.http.post('https://api.example.com/users', {
headers: { 'Content-Type': 'application/json', ...ctx.auth },
body: JSON.stringify({ name: params.name, email: params.email }),
});
return result;
}// Paginated fetching
export default async function(params, ctx) {
const { http, auth, logger } = ctx;
let page = 1;
const allResults = [];
while (true) {
const res = await http.get('https://api.example.com/search', { headers: auth, params: { q: params.query, page } });
allResults.push(...res.results);
if (res.results.length < 20) break;
page++;
}
return { total: allResults.length, results: allResults };
}Authentication
Built-in: Form Provider
Cookie-based authentication (posts credentials, extracts cookies from Set-Cookie header):
auth:
provider: form
config:
login_url: https://api.example.com/login
login_method: POST # Default: POST
login_body: '{"user":"{{username}}","pass":"{{password}}"}' # Optional: JSON body template
login_headers: {} # Optional: Extra login headers
username: your-username
password: your-passwordIf login_body is omitted, credentials are sent as application/x-www-form-urlencoded.
Built-in: OAuth2 Provider
auth:
provider: oauth2
config:
token_url: https://api.example.com/oauth/token
client_id: your-client-id
client_secret: your-client-secret
grant_type: client_credentials # "authorization_code" | "client_credentials"
# For authorization_code:
authorization_url: https://api.example.com/oauth/authorize
redirect_uri: http://localhost:8090/callback
scopes:
- read
- writeCustom Auth Provider
Write an ESM module that exports a class with these methods:
// my-auth-provider.mjs
export default class MyAuthProvider {
async init(config) { /* Called with auth.config */ }
async getAuthHeaders() { /* Return headers object, e.g., { Authorization: "Bearer ..." } */ }
async isValid() { /* Return boolean */ }
async refresh() { /* Re-authenticate */ }
async dispose() { /* Cleanup */ }
async getAuthContext() { /* Optional: return auth metadata */ }
}Then reference it in your config:
auth:
provider: ./my-auth-provider.mjs
config:
token_url: https://api.example.com/token
api_key: your-api-keySee examples/jwt-auth-provider.mjs for a complete JWT example.
Auth Validation & Refresh
The auth system provides two mechanisms to keep credentials valid:
- Proactive polling — Periodically calls a validation endpoint to check if credentials are still valid, and refreshes proactively before they expire
- Reactive refresh — When a tool call receives a 401 response, automatically refreshes credentials and retries the request
auth:
provider: form
config:
login_url: https://api.example.com/login
username: user
password: pass
validation: # Optional: Auth health check endpoint
check_url: https://api.example.com/me
check_method: GET # Default: GET (or POST if check_body is set)
check_headers: {} # Optional: Extra headers for the check request
check_body: "" # Optional: Request body (switches method to POST)
valid_when:
status: 200 # Expected HTTP status code
# jsonpath_not_exists: "$.expired" # Optional: JSONPath that must not exist
# jsonpath_equals: # Optional: JSONPath value assertions
# "$.active": true
# json_match: # Optional: Regex match on response body
# pattern: ".*ok.*"
refresh: # Refresh behavior
on_failure: true # Auto-refresh on 401 (default: true)
retry_count: 3 # Max retries on refresh failure (default: 3)
retry_delay: 5 # Seconds between retries (default: 5)
poll_interval: 300 # Proactive: validate every N seconds (optional, disabled by default)How it works:
| Trigger | Behavior |
|---------|----------|
| poll_interval is set | Every N seconds, calls validation.check_url with current auth headers. If the check fails, triggers refresh() |
| Tool call returns 401 | Automatically calls refresh() and retries the request (up to retry_count + 1 times) |
| No validation defined | Falls back to provider.isValid() for polling (if poll_interval is set) |
| No poll_interval | Polling is disabled; refresh only happens reactively on 401 |
Examples
See the examples/ directory for complete working examples:
jwt-service-config.yaml+jwt-auth-provider.mjs— JWT token auth (custom provider)cookie-service-config.yaml+cookie-auth-provider.mjs— Cookie/session auth with CSRF support (custom provider)oauth-service-config.yaml— OAuth2 client_credentials auth (built-in provider)handler-service-config.yaml— Custom handler tools for multi-API orchestrationtools/create-user-and-profile.mjs— Sequential API calls exampletools/user-summary.mjs— Parallel requests exampletools/search-all.mjs— Paginated fetching example
Architecture
┌──────────────────────────────────────────────────────┐
│ mcp-live-bridge │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Config │──>│ Tool Registry │──>│ MCP Server │ │
│ │ Loader │ │ │ │ (per-session)│ │
│ └──────────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
│ v v │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Request Pipeline │ │
│ │ Auth Headers -> Template -> HTTP -> Transform │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Auth Lifecycle Manager │ │
│ │ Init -> Poll Validate -> Refresh on 401/fail │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Session Manager (per-client transport) │ │
│ │ New transport per session -> route by session │ │
│ └─────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘Development
# Install dependencies
npm install
# Build
npm run build
# Run in watch mode
npm run dev
# Run tests
npm test
# Validate a config file
node dist/index.js validate -c examples/jwt-service-config.yaml
# List tools in a config
node dist/index.js list -c examples/jwt-service-config.yamlRequirements
- Node.js >= 20
