# turbomcp-openapi
OpenAPI to MCP conversion for TurboMCP. Expose REST APIs as MCP tools and resources.
## Overview
This crate allows you to automatically convert an OpenAPI 3.0.x specification (via the `openapiv3` crate) into MCP (Model Context Protocol) tools and resources. This enables AI agents to interact with REST APIs without writing custom handlers.
**Default mapping:**
- `GET` endpoints → MCP Resources (readable content)
- `POST`, `PUT`, `PATCH`, `DELETE` endpoints → MCP Tools (callable operations)
## Quick Start
```rust
use std::path::Path;
use std::time::Duration;
use turbomcp_openapi::{OpenApiHandler, OpenApiProvider};
use turbomcp_server::{ServerBuilder, Transport};
// Load from URL
let provider = OpenApiProvider::from_url("https://api.example.com/openapi.json")
.await?
.with_base_url("https://api.example.com")?
.with_timeout(Duration::from_secs(30)); // Optional, 30s default
// Or load from file
let provider = OpenApiProvider::from_file(Path::new("openapi.yaml"))?
.with_base_url("https://api.example.com")?;
// Or load from string
let provider = OpenApiProvider::from_string(spec_json)?
.with_base_url("https://api.example.com")?;
// Convert to an McpHandler and serve it with any transport
let handler: OpenApiHandler = provider.into_handler();
ServerBuilder::new(handler)
.transport(Transport::stdio())
.serve()
.await?;
```
## Security Features
### SSRF Protection
The provider includes built-in Server-Side Request Forgery (SSRF) protection that blocks requests to:
- **Localhost/loopback**: `127.0.0.0/8`, `::1`, `localhost`
- **Private networks**: `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`
- **Cloud metadata endpoints**: `169.254.169.254` and `169.254.0.0/16`
- **Link-local addresses**: `fe80::/10`
- **Other reserved ranges**: Multicast, broadcast, etc.
This prevents malicious API specs from making requests to internal infrastructure.
### Request Timeouts
All HTTP requests have a configurable timeout (default: 30 seconds) to prevent:
- Slowloris attacks
- Indefinite hangs on unresponsive servers
- Resource exhaustion
```rust
use std::time::Duration;
let provider = OpenApiProvider::from_string(spec)?
.with_base_url("https://api.example.com")?
.with_timeout(Duration::from_secs(10)); // 10 second timeout
// Check current timeout
println!("Timeout: {:?}", provider.timeout());
```
## Custom Route Mapping
You can customize how OpenAPI operations map to MCP types:
```rust
use turbomcp_openapi::{OpenApiProvider, RouteMapping, McpType};
let mapping = RouteMapping::new()
// Default: GET -> Resource
.map_method("GET", McpType::Resource)
// Custom: All /admin/* paths are skipped
.map_pattern(r"/admin/.*", McpType::Skip)?
// Custom: Force specific paths to be tools
.map_rule(["GET"], r"/api/search.*", McpType::Tool, 10)?;
let provider = OpenApiProvider::from_string(spec)?
.with_route_mapping(mapping)
.with_base_url("https://api.example.com")?;
```
## Route Mapping Rules
Rules are evaluated in priority order (highest first):
```rust
use turbomcp_openapi::{RouteRule, McpType};
// Create a rule matching POST/PUT to /users/* paths
let rule = RouteRule::new(McpType::Tool)
.methods(["POST", "PUT"])
.pattern(r"/users/\d+")?
.priority(100); // Higher priority = checked first
```
### McpType Variants
- `McpType::Tool` - Expose as MCP tool (callable operation)
- `McpType::Resource` - Expose as MCP resource (readable content)
- `McpType::Skip` - Don't expose via MCP
## Features
- **OpenAPI 3.0.x Support** - Parse both JSON and YAML specifications (via `openapiv3`)
- **Multiple Loading Methods** - From URL, file path, or string
- **Configurable Mapping** - Customize how operations map to MCP types
- **Regex Pattern Matching** - Route rules support regex path patterns
- **Parameter Handling** - Path, query, header, and cookie parameters
- **Request Body Support** - JSON request bodies converted to tool inputs
- **HTTP Client Integration** - Built-in reqwest client for API calls
- **Custom Client Support** - Provide your own configured reqwest::Client
- **SSRF Protection** - Built-in protection against server-side request forgery
- **Request Timeouts** - Configurable timeouts (default: 30 seconds)
## API Reference
### OpenApiProvider
The main entry point for loading and configuring OpenAPI specs:
```rust
impl OpenApiProvider {
// Loading methods
pub fn from_spec(spec: OpenAPI) -> Self;
pub fn from_string(content: &str) -> Result<Self>;
pub fn from_file(path: &Path) -> Result<Self>;
pub async fn from_url(url: &str) -> Result<Self>;
// Configuration
pub fn with_base_url(self, base_url: &str) -> Result<Self>;
pub fn with_route_mapping(self, mapping: RouteMapping) -> Self;
pub fn with_client(self, client: reqwest::Client) -> Self;
pub fn with_timeout(self, timeout: Duration) -> Self;
// Inspection
pub fn title(&self) -> &str;
pub fn version(&self) -> &str;
pub fn timeout(&self) -> Duration;
pub fn operations(&self) -> &[ExtractedOperation];
pub fn tools(&self) -> impl Iterator<Item = &ExtractedOperation>;
pub fn resources(&self) -> impl Iterator<Item = &ExtractedOperation>;
// Conversion
pub fn into_handler(self) -> OpenApiHandler;
}
```
### OpenApiHandler
Implements `turbomcp_core::handler::McpHandler` for use with TurboMCP servers.
The handler exposes:
- `server_info()` — returns the spec's `info.title` / `info.version`
- `list_tools()` — one `Tool` per non-GET operation (subject to route mapping); `meta` includes the original `method`, `path`, and `operationId`
- `list_resources()` — one `Resource` per GET operation (subject to route mapping); `mime_type` is `application/json`
- `list_prompts()` — always empty (OpenAPI has no prompt concept)
- `call_tool(name, args, ctx)` — dispatches an HTTP request for the matching tool, with SSRF validation
- `read_resource(uri, ctx)` — issues the GET for the matching resource URI
- `get_prompt(...)` — always returns `prompt_not_found`
Tool names use `operation_id` when present, otherwise `{method}_{path}` with `/`
replaced by `_` and `{}` stripped. Resource URIs are
`openapi://{method}{path}` (e.g. `openapi://get/users/{id}`).
## Error Types
```rust
pub enum OpenApiError {
FetchError(reqwest::Error), // Failed to fetch spec from URL
ParseError(String), // Failed to parse spec (JSON/YAML)
IoError(std::io::Error), // Failed to read file
InvalidUrl(url::ParseError), // Invalid URL
InvalidPattern(regex::Error), // Invalid regex pattern
ApiError(String), // API call returned error
MissingParameter(String), // Required parameter missing
InvalidParameter(String, String), // Invalid parameter value
OperationNotFound(String), // Operation not found
NoBaseUrl, // Base URL not configured
SsrfBlocked(String), // SSRF protection blocked request
Timeout(u64), // Request timed out
}
```
## Example
Given this OpenAPI spec:
```yaml
openapi: 3.0.0
info:
title: Pet Store
version: 1.0.0
paths:
/pets:
get:
operationId: listPets
summary: List all pets
post:
operationId: createPet
summary: Create a pet
/pets/{id}:
get:
operationId: getPet
summary: Get a pet by ID
delete:
operationId: deletePet
summary: Delete a pet
```
The handler exposes:
**Resources:**
- `openapi://get/pets` (listPets)
- `openapi://get/pets/{id}` (getPet)
**Tools:**
- `createPet` - Create a pet
- `deletePet` - Delete a pet
## Running the Example
```bash
cargo run -p turbomcp-openapi --example petstore
```
## Schema Handling Notes
- **`$ref` resolution**: references into `components.schemas` are resolved
recursively and inlined into the emitted MCP tool/resource schemas, so
consumers never see dangling pointers. Self-referential schemas are
handled — the expander detects cycles and preserves the innermost `$ref`
unchanged so output stays finite.
- **Schema composition**: `allOf`, `oneOf`, `anyOf`, `discriminator`, and
`nullable` round-trip through `serde_json` as JSON Schema keywords, which
downstream MCP clients that speak JSON Schema 2020-12 can consume directly.
- **Parameter `content`**: only `schema`-form parameters are extracted; the
content-type variant form is not yet converted.
- **Security schemes**: declared schemes are recognized but not automatically
applied to outgoing requests; callers supply auth headers manually.
## License
MIT