Skip to content

Expose OpenAPI as MCP tools

In this lab, you will experiment with yet another interesting feature of agentgateway: its ability to expose OpenAPI endpoints as MCP tools.

Unauthenticated tool calls

Review the following, trimmed, OpenAPI specification for the GitHub API:

github-search.openapi.json
{
  "openapi": "3.0.3",
  "info": {
    "title": "GitHub Search (trimmed)",
    "description": "A deliberately small subset of the GitHub REST API for the credential-injection lab. The full GitHub OpenAPI description is enormous and would generate hundreds of MCP tools, which would ironically re-teach the tool-sprawl lesson from step 2. This file has three operations. Regenerate or extend it from https://github.com/github/rest-api-description if you need more.",
    "version": "1.0.0"
  },
  "servers": [{ "url": "https://api.github.com" }],
  "paths": {
    "/search/repositories": {
      "get": {
        "operationId": "search_repositories",
        "summary": "Search GitHub repositories by keyword, ranked by popularity. GitHub has no trending API, so approximate it: pass 'q' as a plain keyword string AND set sort='stars' to surface the most-starred repos. Example: q='AI agents', sort='stars'.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "REQUIRED. A single plain-text keyword STRING (not an object), the field is named q. Keep it to a few keywords, e.g. 'AI agents' or 'LLM agent framework' -- avoid stars:> filters, sort by popularity instead.",
            "schema": { "type": "string", "example": "AI agents" }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "ALWAYS set this to 'stars' to rank by popularity. GitHub's default is relevance, which surfaces obscure low-star repos.",
            "schema": { "type": "string", "enum": ["stars", "forks", "updated"], "default": "stars", "example": "stars" }
          },
          {
            "name": "order",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "enum": ["desc", "asc"] }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Set this to 3. GitHub returns a large object per repo and defaults to 30, which a small local model cannot digest -- always request just 3.",
            "schema": { "type": "integer", "maximum": 100, "example": 3 }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching repositories",
            "content": { "application/json": { "schema": { "type": "object" } } }
          }
        }
      }
    },
    "/repos/{owner}/{repo}": {
      "get": {
        "operationId": "get_repository",
        "summary": "Get details for one repository, including stars, description and topics.",
        "parameters": [
          {
            "name": "owner",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          },
          {
            "name": "repo",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Repository detail",
            "content": { "application/json": { "schema": { "type": "object" } } }
          }
        }
      }
    },
    "/rate_limit": {
      "get": {
        "operationId": "get_rate_limit",
        "summary": "Show the current API rate limit. Useful for proving whether a credential was injected: 60 per hour means unauthenticated, 5000 means the token was attached.",
        "responses": {
          "200": {
            "description": "Rate limit status",
            "content": { "application/json": { "schema": { "type": "object" } } }
          }
        }
      }
    }
  }
}

The specification exposes three API calls:

  • search_repositories
  • get_repository
  • get_rate_limit

Next, review the agentgateway configuration:

no-github-token.yaml
# yaml-language-server: $schema=https://agentgateway.dev/schema/config
config:
  database:
    url: sqlite://./data.db
llm:
  models:
  - name: trend-pro
    provider: openAI
    params:
      baseUrl: http://localhost:11434/v1
      model: qwen3:8b
      apiKey: ollama-does-not-check-this
mcp:
  targets:
  - name: github
    openapi:
      host: https://api.github.com
      schema:
        file: configs/github-search.openapi.json
no-github-token-gemini.yaml
# yaml-language-server: $schema=https://agentgateway.dev/schema/config
config:
  database:
    url: sqlite://./data.db
llm:
  models:
  - name: trend-pro
    provider: gemini
    params:
      model: gemini-3.5-flash-lite
      apiKey: "$GEMINI_API_KEY"
mcp:
  targets:
  - name: github
    openapi:
      host: https://api.github.com
      schema:
        file: configs/github-search.openapi.json

Above, note how the mcp target using an openapi stanza, which references the OpenAPI specification.

In one terminal, start agentgateway:

agentgateway -f configs/no-github-token.yaml

Make sure GEMINI_API_KEY is still set in this terminal, then:

agentgateway -f configs/no-github-token-gemini.yaml

In a second terminal, run the agent with the query about the GitHub rate limit:

python3 agent/trendwatch.py "what is my GitHub rate limit?"

You should receive an answer about the rate limit being 60 requests per hour. This is the unauthenticated rate limit.

Authenticated tool call

Agentgateway provides a mechanism to configure a backend target with credentials. In this case, we wish to send a Personal Access Token to the GitHub API backend.

In the first terminal, press Ctrl+C to terminate agentgateway.

Review the agentgateway configuration:

credential-injection.yaml
# yaml-language-server: $schema=https://agentgateway.dev/schema/config
config:
  database:
    url: sqlite://./data.db
llm:
  models:
  - name: trend-pro
    provider: openAI
    params:
      baseUrl: http://localhost:11434/v1
      model: qwen3:8b
      apiKey: ollama-does-not-check-this
mcp:
  policies:
    backendAuth:
      key:
        value: $GITHUB_TOKEN
  targets:
  - name: github
    openapi:
      host: https://api.github.com
      schema:
        file: configs/github-search.openapi.json
credential-injection-gemini.yaml
# yaml-language-server: $schema=https://agentgateway.dev/schema/config
config:
  database:
    url: sqlite://./data.db
llm:
  models:
  - name: trend-pro
    provider: gemini
    params:
      model: gemini-3.5-flash-lite
      apiKey: "$GEMINI_API_KEY"
mcp:
  policies:
    backendAuth:
      key:
        value: $GITHUB_TOKEN
  targets:
  - name: github
    openapi:
      host: https://api.github.com
      schema:
        file: configs/github-search.openapi.json

The main thing to note is the static key configured under backendAuth: the key is configured to the value of the environment variable GITHUB_TOKEN.

Create a GitHub token

Visit GitHub Personal access tokens and generate a new token for yourself:

  • Give it a name
  • For "Repository access", just select "Public repositories"
  • No need to add any permissions
  • Click "Generate token"

Copy the generated token to your clipboard so that you can configure the requisite environment variable:

export GITHUB_TOKEN="<paste your token here>"

Start the agentgateway with this configuration:

agentgateway -f configs/credential-injection.yaml

Make sure GEMINI_API_KEY is still set in this terminal, then:

agentgateway -f configs/credential-injection-gemini.yaml

In the second terminal, repeat the query, the question now is in the context of the credentials represented by the supplied key:

python3 agent/trendwatch.py "what is my GitHub rate limit?"

The reply should indicate that the rate limit is more generous for an authenticated user: 5000 requests per hour.

Summary

In this lab, you've seen how easily agentgateway exposes an MCP server from an OpenAPI specification. It supports a variety of backend authentication mechanisms; read more about it here.