diff --git a/.github/assets/images/repo-header-a4.png b/.github/assets/images/repo-header-a4.png new file mode 100644 index 0000000..3672ea5 Binary files /dev/null and b/.github/assets/images/repo-header-a4.png differ diff --git a/.github/assets/images/repo-header-a4.txt b/.github/assets/images/repo-header-a4.txt new file mode 100644 index 0000000..a1f671f --- /dev/null +++ b/.github/assets/images/repo-header-a4.txt @@ -0,0 +1,15 @@ +Image Description Sheet +----------------------- + +File Name: repo-header-a4.png +Format: PNG +Dimensions: 830 x 173 pixels + +Short Description: +A banner image featuring the word "Openapi" on the left, Various tech figures in the background, +and the Openapi logo on the right. + +Purpose: +This image is intended to be used as a header/banner for documentation or repository README. +It visually associates linux tools with the Openapi MCP, +giving a professional and modern look to the project materials. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 6d1ab7b..12d4e12 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,6 +1,6 @@ ## Quick context for AI coding agents -This repository implements the OpenAPI.com MCP gateway (FastAPI + FastMCP). The server acts as a proxy that forwards a client's Bearer token to downstream OpenAPI services and exposes MCP tools implemented under `src/openapi_mcp_sdk/apis/`. +This repository implements the Openapi.com MCP gateway (FastAPI + FastMCP). The server acts as a proxy that forwards a client's Bearer token to downstream Openapi services and exposes MCP tools implemented under `src/openapi_mcp_sdk/apis/`. Key files - `src/openapi_mcp_sdk/main.py` โ€” application entry point. Mounts the MCP app and contains HTTP endpoints `/callbacks` and `/status/{request_id}`. Shows how token query params are converted into an Authorization header. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..70a2002 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,38 @@ + +name: build + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.9", "3.10", "3.11"] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install flake8 pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest + run: | + pytest diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml new file mode 100644 index 0000000..0782831 --- /dev/null +++ b/.github/workflows/pylint.yml @@ -0,0 +1,23 @@ +name: Pylint + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + - name: Analysing the code with pylint + run: | + pylint $(git ls-files '*.py') diff --git a/.gitignore b/.gitignore index 621398b..34568e9 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ re-deploy.sh # Local build artifacts .install.stamp +.python-version # Generated by integration tests (contains token) .mcp.json diff --git a/README.md b/README.md index 2e51372..38ec4e2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,22 @@ -# openapi-mcp-sdk +
+ + Openapi MCP + -**`openapi-mcp-sdk`** is the official [openapi.com](https://openapi.com/) MCP SDK. +

๐Ÿ” Openapiยฎ MCP

+

The official Python MCP SDK and ready-to-run MCP server for Openapiยฎ

+ +[![PyPI](https://img.shields.io/pypi/v/openapi-mcp-sdk.svg)](https://pypi.org/project/openapi-mcp-sdk/) +[![Python](https://img.shields.io/badge/python-3.13%2B-blue.svg)](https://www.python.org/) +[![License](https://img.shields.io/github/license/openapi/mcp-server)](LICENSE) +[![MCP](https://img.shields.io/badge/protocol-MCP-1f6feb.svg)](https://modelcontextprotocol.io/) +
+[![Linux Foundation Member](https://img.shields.io/badge/Linux%20Foundation-Silver%20Member-003778?logo=linux-foundation&logoColor=white)](https://www.linuxfoundation.org/about/members) +
+ +# Overview + +Welcome to **`openapi-mcp-sdk`**, this is the official [openapi.com](https://openapi.com/) MCP SDK. It ships as a Python package on [PyPI](https://pypi.org/project/openapi-mcp-sdk/) and can be used in two ways: @@ -10,8 +26,6 @@ can be used in two ways: custom MCP server that wraps openapi.com APIs with your own logic, tools, or auth layer on top. ---- - ## Environment variables All configuration is done through environment variables. None are required to @@ -19,13 +33,13 @@ start the server โ€” defaults are suitable for local use. ### Core -| Variable | Default | Description | -|---|---|---| -| `MCP_PORT` | `8080` | HTTP port the server listens on | -| `MCP_ENV` | `production` | Runtime environment: `dev` \| `staging` \| `production` | -| `MCP_BASE_URL` | `https://mcp.openapi.com` | Public URL of this server (used to build callback and file download URLs) | -| `MCP_CALLBACK_URL` | `$MCP_BASE_URL/callbacks` | Explicit callback URL sent to async APIs | -| `MCP_OPENAPI_ENV` | _(empty)_ | OpenAPI environment: `dev`, `test`, `sandbox` (alias of `test`), or empty for production | +| Variable | Default | Description | +|---|---|------------------------------------------------------------------------------------------| +| `MCP_PORT` | `8080` | HTTP port the server listens on | +| `MCP_ENV` | `production` | Runtime environment: `dev` \| `staging` \| `production` | +| `MCP_BASE_URL` | `http://localhost:8080` | Public URL of this server (used to build callback and file download URLs) | +| `MCP_CALLBACK_URL` | `$MCP_BASE_URL/callbacks` | Explicit callback URL sent to async APIs | +| `MCP_OPENAPI_ENV` | _(empty)_ | Openapi environment: `dev`, `test`, `sandbox` (alias of `test`), or empty for production | ### Storage @@ -53,8 +67,6 @@ Used to share async callback results across multiple server instances. See [`docs/env/`](docs/env/) for per-environment configuration guides (local, Docker, AWS, GCP, Kubernetes). ---- - ## Features - **Secure proxy**: Pass-through of the Bearer Token provided by the client, without direct handling of sensitive credentials. @@ -62,8 +74,6 @@ Docker, AWS, GCP, Kubernetes). - **MCP-compatible**: Designed according to MCP protocol best practices. - **Modular**: All API call logic and tool registration is centralized in [`mcp_core.py`](src/openapi_mcp_sdk/mcp_core.py). ---- - ## Quick Start โ€” run from PyPI (recommended) No cloning, no virtual environment. Start the server in one command: @@ -98,34 +108,7 @@ Commands: token Generate or inspect an openapi.com Bearer token [coming soon] ``` ---- -## Local launcher script - -Copy the block below, paste it into your terminal, and press Enter. It will -create a `mcp-server.sh` file in the current directory and launch the server: - -```bash -cat > mcp-server.sh << 'EOF' -#!/bin/bash -# ============================================================ -# Openapi.com MCP Server โ€” local launcher -# Edit the variables below, then run: bash mcp-server.sh -# ============================================================ - -export MCP_PORT="${MCP_PORT:-8080}" -uvx openapi-mcp-sdk server -EOF -bash mcp-server.sh -``` - -Next time, just run: - -```bash -bash mcp-server.sh -``` - ---- ## Running with Docker @@ -144,7 +127,7 @@ docker compose logs -f mcp The server will be accessible at `http://localhost:8080`. ---- + ## Debug and Development @@ -162,13 +145,13 @@ server listens on `http://0.0.0.0:8080`. To test endpoints manually: ```bash -curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8080/mcp/ +curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8080 ``` The server prints request headers and parameters to stdout โ€” no extra configuration needed. ---- + ### Remote debugging with VS Code and Docker (one click) @@ -218,7 +201,7 @@ required. Just send a new request and the updated code runs. | MCP server | `localhost:8080` | `0.0.0.0:8080` | | debugpy listener | `localhost:5678` | `0.0.0.0:5678` | ---- + ## Authentication @@ -248,7 +231,7 @@ before the request reaches the MCP layer, so the behaviour is identical. > MCP playgrounds, and any client whose configuration only accepts a plain URL > without a headers field. ---- + ## MCP Client Configuration (VS Code) @@ -289,7 +272,7 @@ before the request reaches the MCP layer, so the behaviour is identical. - Open the Copilot chat and type `@workspace`. - Use the tools exposed by the MCP server. ---- + ## File storage @@ -319,7 +302,7 @@ uvx openapi-mcp-sdk server > The directory is created automatically on first use. For the download links to > work correctly, `MCP_BASE_URL` must point to the public address of the server -> (default: `https://mcp.openapi.com`). +> (default: `http://localhost:8080`). ### Cloud storage backends @@ -331,7 +314,7 @@ uvx openapi-mcp-sdk server See [`docs/env/`](docs/env/) for full per-environment configuration guides. ---- + ## Deployment environments @@ -343,7 +326,7 @@ See [`docs/env/`](docs/env/) for full per-environment configuration guides. | Amazon Web Services | [`docs/env/aws.md`](docs/env/aws.md) | | Kubernetes | [`docs/env/kubernetes.md`](docs/env/kubernetes.md) | ---- + ## Project Structure @@ -357,7 +340,7 @@ See [`docs/env/`](docs/env/) for full per-environment configuration guides. - [`.vscode/launch.json`](.vscode/launch.json): VS Code debug configuration (attach to debugpy). - [`docs/`](docs/): Documentation and example configurations. ---- + ## Adding New Tools/APIs @@ -374,7 +357,7 @@ See [`docs/env/`](docs/env/) for full per-environment configuration guides. ``` 2. Restart the server to apply the changes. ---- + ## Security Notes @@ -382,7 +365,7 @@ See [`docs/env/`](docs/env/) for full per-environment configuration guides. - The server expects the Bearer Token to be provided by the client via HTTP headers. - All API calls are proxied using the token provided by the client. ---- + ## Useful Resources @@ -390,7 +373,6 @@ See [`docs/env/`](docs/env/) for full per-environment configuration guides. - [fastmcp](https://pypi.org/project/fastmcp/) - [openapi.com](https://openapi.com/) ---- ## Contributing diff --git a/docs/env/README.md b/docs/env/README.md index 4754d4f..1f9ec79 100644 --- a/docs/env/README.md +++ b/docs/env/README.md @@ -10,21 +10,21 @@ cache backend, and any platform-specific notes. ## Quick reference โ€” all environment variables -| Variable | Default | Description | -|---|---|---| -| `MCP_PORT` | `8080` | HTTP port the server listens on | -| `MCP_ENV` | `production` | Runtime environment: `dev` \| `staging` \| `production` | -| `MCP_BASE_URL` | `https://mcp.openapi.com` | Public URL of this server (used in callback URLs) | -| `MCP_CALLBACK_URL` | `$MCP_BASE_URL/callbacks` | Full callback URL sent to async APIs | -| `MCP_OPENAPI_ENV` | _(empty)_ | OpenAPI environment: `dev`, `test`, `sandbox` (alias of `test`), or empty for production | -| `MCP_STORAGE_BACKEND` | `local` | Storage backend: `local` \| `gcs` \| `s3` | -| `MCP_STORAGE_PATH` | `./openapi_storage` | Local filesystem path for `local` backend | -| `MCP_STORAGE_BUCKET` | _(required for cloud)_ | Bucket/container name for `gcs` or `s3` backend | -| `MCP_STORAGE_REGION` | _(required for s3)_ | AWS region for the S3 bucket | -| `MCP_CACHE_BACKEND` | `none` | Cache for async callbacks: `none` \| `memcached` \| `redis` | -| `MCP_CACHE_HOST` | _(none)_ | Memcached host (used when `MCP_CACHE_BACKEND=memcached`) | -| `MCP_CACHE_PORT` | `11211` | Memcached port | -| `MCP_CACHE_URL` | _(none)_ | Redis connection URL (used when `MCP_CACHE_BACKEND=redis`) | +| Variable | Default | Description | +|---|---|------------------------------------------------------------------------------------------| +| `MCP_PORT` | `8080` | HTTP port the server listens on | +| `MCP_ENV` | `production` | Runtime environment: `dev` \| `staging` \| `production` | +| `MCP_BASE_URL` | `http://localhost:8080` | Public URL of this server (used in callback URLs) | +| `MCP_CALLBACK_URL` | `$MCP_BASE_URL/callbacks` | Full callback URL sent to async APIs | +| `MCP_OPENAPI_ENV` | _(empty)_ | Openapi environment: `dev`, `test`, `sandbox` (alias of `test`), or empty for production | +| `MCP_STORAGE_BACKEND` | `local` | Storage backend: `local` \| `gcs` \| `s3` | +| `MCP_STORAGE_PATH` | `./openapi_storage` | Local filesystem path for `local` backend | +| `MCP_STORAGE_BUCKET` | _(required for cloud)_ | Bucket/container name for `gcs` or `s3` backend | +| `MCP_STORAGE_REGION` | _(required for s3)_ | AWS region for the S3 bucket | +| `MCP_CACHE_BACKEND` | `none` | Cache for async callbacks: `none` \| `memcached` \| `redis` | +| `MCP_CACHE_HOST` | _(none)_ | Memcached host (used when `MCP_CACHE_BACKEND=memcached`) | +| `MCP_CACHE_PORT` | `11211` | Memcached port | +| `MCP_CACHE_URL` | _(none)_ | Redis connection URL (used when `MCP_CACHE_BACKEND=redis`) | ### Legacy variables (GCP Cloud Run โ€” deprecated) diff --git a/docs/env/gcp.md b/docs/env/gcp.md index 2ddae0a..7445126 100644 --- a/docs/env/gcp.md +++ b/docs/env/gcp.md @@ -26,8 +26,8 @@ Internet โ†’ Cloud Run (openapi-mcp-sdk server) # Core MCP_PORT=8080 MCP_ENV=production -MCP_BASE_URL=https://mcp.openapi.com -MCP_CALLBACK_URL=https://mcp.openapi.com/callbacks +MCP_BASE_URL=https://mcp.example.com +MCP_CALLBACK_URL=https://mcp.example.com/callbacks MCP_OPENAPI_ENV= # dev, test, sandbox (alias of test), or empty for production # Storage @@ -81,7 +81,7 @@ spec: - name: MCP_PORT value: "8080" - name: MCP_BASE_URL - value: "https://mcp.openapi.com" + value: "https://mcp.example.com" - name: MCP_STORAGE_BACKEND value: "gcs" - name: MCP_STORAGE_BUCKET diff --git a/docs/env/local.md b/docs/env/local.md index c224895..3fff676 100644 --- a/docs/env/local.md +++ b/docs/env/local.md @@ -75,7 +75,7 @@ Options: export MCP_BASE_URL=https://abc123.ngrok.io ``` -If `MCP_BASE_URL` is not set the server defaults to `https://mcp.openapi.com`, which +If `MCP_BASE_URL` is not set the server defaults to `http://localhost:8080`, which will not work for local callbacks. --- @@ -103,7 +103,7 @@ source local.env && openapi-mcp-sdk server cat > mcp-server.sh << 'EOF' #!/bin/bash export MCP_PORT="${MCP_PORT:-8080}" -export MCP_BASE_URL="${MCP_BASE_URL:-https://mcp.openapi.com}" +export MCP_BASE_URL="${MCP_BASE_URL:-http://localhost:8080}" export MCP_STORAGE_BACKEND="${MCP_STORAGE_BACKEND:-local}" export MCP_STORAGE_PATH="${MCP_STORAGE_PATH:-./openapi_storage}" diff --git a/docs/project.md b/docs/project.md index 575c5d8..04fe348 100644 --- a/docs/project.md +++ b/docs/project.md @@ -116,7 +116,7 @@ def make_api_call(ctx: Context, method: str, url: str, **kwargs) -> Any: # --- Inizializzazione del Server MCP --- mcp = FastMCP( - name="OpenAPI.com Gateway", + name="Openapi.com Gateway", instructions="Questo server fornisce un gateway unificato per diversi servizi di openapi.com." ) @@ -215,7 +215,7 @@ if __name__ == "__main__": "servers": { "openapi.com": { "type": "http", - "url": "http://INDIRIZZO_IP_DEL_TUO_SERVER:8000/mcp/", + "url": "http://INDIRIZZO_IP_DEL_TUO_SERVER:8000", "headers": { "Authorization": "Bearer IL_TUO_BEARER_TOKEN_DI_PRODUZIONE" } diff --git a/docs/claude_desktop_config.json b/docs/settings/claude_desktop_config.json similarity index 84% rename from docs/claude_desktop_config.json rename to docs/settings/claude_desktop_config.json index 39a636c..2766748 100644 --- a/docs/claude_desktop_config.json +++ b/docs/settings/claude_desktop_config.json @@ -4,7 +4,7 @@ "command": "npx", "args": [ "mcp-remote", - "http://127.0.0.1:8000/mcp/", + "http://127.0.0.1:8000", "--header", "Authorization: Bearer 68614e09843b57c48308abc5" ] diff --git a/docs/mcp.json b/docs/settings/mcp.json similarity index 87% rename from docs/mcp.json rename to docs/settings/mcp.json index bb955c8..065b49f 100644 --- a/docs/mcp.json +++ b/docs/settings/mcp.json @@ -8,7 +8,7 @@ "servers": { "openapi.com": { "type": "http", - "url": "http://127.0.0.1:8000/mcp/", + "url": "http://127.0.0.1:8000", "headers": { "Authorization": "Bearer ${input:OpenapiToken}" } diff --git a/docs/settings.json b/docs/settings/settings.json similarity index 100% rename from docs/settings.json rename to docs/settings/settings.json diff --git a/docs/testing.md b/docs/testing.md index d70503b..fe4a79d 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -112,7 +112,7 @@ ngrok http 8080 # Terminal 3 โ€” run tests export OPENAI_API_KEY=sk-... export OPENAPI_SANDBOX_TOKEN=your_sandbox_token -MCP_URL=https://abcd1234.ngrok-free.app/mcp/ SANDBOX=1 make test-openai-sandbox +MCP_URL=https://abcd1234.ngrok-free.app SANDBOX=1 make test-openai-sandbox ``` --- diff --git a/scripts/try-claude.sh b/scripts/try-claude.sh index b7908ec..203b7f5 100755 --- a/scripts/try-claude.sh +++ b/scripts/try-claude.sh @@ -10,7 +10,7 @@ WORK_DIR="$(mktemp -d /tmp/openapi-try-XXXXXX)" SERVER_PID="" -# Sandbox mode: SANDBOX=1 uses OPENAPI_SANDBOX_TOKEN and the test OpenAPI environment +# Sandbox mode: SANDBOX=1 uses OPENAPI_SANDBOX_TOKEN and the test Openapi environment SANDBOX="${SANDBOX:-0}" if [ "$SANDBOX" = "1" ]; then TOKEN="${OPENAPI_SANDBOX_TOKEN:-}" diff --git a/src/openapi_mcp_sdk/apis/info.py b/src/openapi_mcp_sdk/apis/info.py index c03d96b..af56436 100644 --- a/src/openapi_mcp_sdk/apis/info.py +++ b/src/openapi_mcp_sdk/apis/info.py @@ -27,7 +27,7 @@ _FASTMCP_VERSION = "unknown" _SERVER_VERSION = "0.2.0" -_SERVER_NAME = "OpenAPI.com MCP Gateway" +_SERVER_NAME = "Openapi.com MCP Gateway" def _mask_token(token: str) -> str: diff --git a/src/openapi_mcp_sdk/memory_store.py b/src/openapi_mcp_sdk/memory_store.py index dcd9bea..ea5334e 100644 --- a/src/openapi_mcp_sdk/memory_store.py +++ b/src/openapi_mcp_sdk/memory_store.py @@ -39,8 +39,6 @@ OPENAPI_HOST_PREFIX = f"{MCP_OPENAPI_ENV}." if MCP_OPENAPI_ENV else "" MCP_BASE_URL = os.getenv("MCP_BASE_URL", "http://localhost:8080") -if MCP_STORAGE_BUCKET and "MCP_BASE_URL" not in os.environ: - MCP_BASE_URL = "https://" + MCP_STORAGE_BUCKET.replace("-", ".") callbackUrl = os.getenv("MCP_CALLBACK_URL") if not callbackUrl: diff --git a/tests/docker/test-compose-up.sh b/tests/docker/test-compose-up.sh index 2a3e86e..50dcb33 100755 --- a/tests/docker/test-compose-up.sh +++ b/tests/docker/test-compose-up.sh @@ -142,7 +142,7 @@ assert_contains "initialize โ†’ jsonrpc result" "$INIT_BODY" '"result"' assert_contains "initialize โ†’ protocolVersion" "$INIT_BODY" '"protocolVersion"' assert_contains "initialize โ†’ serverInfo" "$INIT_BODY" '"serverInfo"' assert_contains "initialize โ†’ capabilities" "$INIT_BODY" '"capabilities"' -assert_contains "initialize โ†’ server name" "$INIT_BODY" 'OpenAPI' +assert_contains "initialize โ†’ server name" "$INIT_BODY" 'Openapi' SESSION_ID=$(extract_session_id) if [ -n "$SESSION_ID" ]; then @@ -190,7 +190,7 @@ CALL_BODY=$(parse_mcp_response "$CALL_RAW") assert_contains "tools/call โ†’ jsonrpc result" "$CALL_BODY" '"result"' assert_contains "tools/call โ†’ content array" "$CALL_BODY" '"content"' assert_contains "tools/call โ†’ isError false" "$CALL_BODY" 'false' -assert_contains "tools/call โ†’ server name in payload" "$CALL_BODY" 'OpenAPI' +assert_contains "tools/call โ†’ server name in payload" "$CALL_BODY" 'Openapi' assert_contains "tools/call โ†’ status ok in payload" "$CALL_BODY" 'status' # ============================================================================= diff --git a/tests/integration/run-vs-codex.sh b/tests/integration/run-vs-codex.sh index e82f5af..8e196a6 100755 --- a/tests/integration/run-vs-codex.sh +++ b/tests/integration/run-vs-codex.sh @@ -59,7 +59,7 @@ fi codex mcp remove "$MCP_SERVER_NAME" 2>/dev/null || true codex mcp add "$MCP_SERVER_NAME" \ - --url "http://localhost:8080/mcp/" \ + --url "http://localhost:8080" \ --bearer-token-env-var "OPENAPI_MCP_TOKEN" # --- start MCP server in background --- diff --git a/tests/integration/run-vs-openai.sh b/tests/integration/run-vs-openai.sh index a63cedc..bece8f1 100755 --- a/tests/integration/run-vs-openai.sh +++ b/tests/integration/run-vs-openai.sh @@ -22,8 +22,8 @@ fi # MCP_URL: the URL OpenAI will use to reach the MCP server. # OpenAI cloud CANNOT reach localhost โ€” expose the server first: # ngrok http 8080 -# MCP_URL=https://xxxx.ngrok-free.app/mcp/ make test-openai -MCP_URL="${MCP_URL:-http://localhost:8080/mcp/}" +# MCP_URL=https://xxxx.ngrok-free.app make test-openai +MCP_URL="${MCP_URL:-http://localhost:8080}" cleanup() { [ -n "$SERVER_PID" ] && kill "$SERVER_PID" 2>/dev/null || true @@ -53,7 +53,7 @@ if echo "$MCP_URL" | grep -qE "localhost|127\.0\.0\.1"; then echo "WARNING: MCP_URL='$MCP_URL' โ€” OpenAI cloud cannot reach localhost." echo " Expose the server with ngrok and set MCP_URL to the public URL:" echo " ngrok http 8080" - echo " MCP_URL=https://xxxx.ngrok-free.app/mcp/ make test-openai" + echo " MCP_URL=https://xxxx.ngrok-free.app make test-openai" echo "" fi