Cookbook: bridging an existing REST API
If the application already maintains an OpenAPI document, exposing a handful of read endpoints as MCP tools can be zero-duplication config — no hand-written tool class at all. Full reference: OpenAPI bridge.
1. Start read-only, from a local spec file
'rasuvaeff/yii3-mcp' => [
'openapi' => [
'spec_path' => __DIR__ . '/../resources/openapi.json',
'base_url' => 'https://api.example.com',
'operations' => ['getBlogTags', 'getPage'],
'headers' => ['Authorization' => 'Bearer ' . getenv('MCP_API_TOKEN')],
'safe_methods_only' => true, // build fails if a non-GET operation sneaks into the list
],
],A local file avoids the two extra concerns a URL spec brings in (network failure modes, TTL-bounded staleness — see OpenAPI bridge: caching the URL spec), and safe_methods_only is a second line of defense while you're still deciding what should be write-enabled.
2. Verify what actually got bridged
./yii mcp:listConfirm getBlogTags/getPage show up with the arguments and output schema you expect — Operations: mcp:list goes through the same in-process path a real client uses, so a spec indexing failure (bad $ref, unsupported parameter shape — see OpenAPI bridge: spec parsing constraints) shows up here as a build error rather than silently.
3. Rename for the agent's benefit
Generated operationIds are rarely LLM-friendly:
'openapi' => [
// ...
'tool_names' => ['getBlogTags' => 'blog_tags_list'],
],From here on, every reference to this tool — interceptors, visibility patterns, an audit/RBAC rule — must use the renamed name, blog_tags_list, not getBlogTags. The allow-list and the actual HTTP call still key off operationId internally; only what the client sees changes.
4. Add a dry-run for anything write-shaped
Before enabling a mutating operation for real, preview what it would send:
'openapi' => [
// ...
'operations' => ['getBlogTags', 'getPage', 'createComment'],
'dry_run' => ['createComment'],
],A call to the createComment tool with dryRun: true returns the planned request instead of executing it. This is orthogonal to safe_methods_only — it does not itself expose createComment, it only adds a preview mode once the operation is in the allow-list. See OpenAPI bridge: dry run for the fail-closed guarantees around it.
5. Move from a service token to delegated identity, if needed
The setup so far calls the upstream API with one static credential (headers) — every MCP client shares it, and the upstream sees no per-user distinction. If the tools should act as the calling application user rather than a shared service account:
'openapi' => [
// ...
'identity_provider' => AppExecutionIdentityProvider::class,
'delegated_header_provider' => AppDelegatedHeaderProvider::class,
],Read OpenAPI bridge: delegated authorization before flipping this on — the header provider receives only the resolved identity, never the raw MCP secret or the inbound Authorization header, and a provider failure stops the call before any HTTP request is made.
6. Move the spec to a URL once it's stable
'openapi' => [
'spec_path' => 'https://api.example.com/rest/json-url',
'spec_headers' => [], // separate credential scope from `headers`
'cache_ttl' => 60,
],Now the bridge always reflects the live API surface — at the cost of the staleness window cache_ttl introduces (a removed operation can remain callable for up to the TTL) and a new network failure mode mcp:doctor --probe can check ahead of time (see Cookbook: debugging with mcp:doctor).