diff --git a/distribution/tutorials/README.md b/distribution/tutorials/README.md index 8f40e98376..6e0d82988d 100644 --- a/distribution/tutorials/README.md +++ b/distribution/tutorials/README.md @@ -25,6 +25,11 @@ If your APIs use XML as input or output, this tutorial provides useful configura Learn how to use Membrane in more advanced scenarios. Topics include path rewriting, scripting, conditions and more. +## [AI / MCP](ai/mcp) + +Expose Membrane as an MCP server for AI clients, inspect recent API traffic, and protect the MCP endpoint with an API key. + + ## [SOAP Web Services (Legacy)](soap) If you need to integrate legacy SOAP Web Services, this tutorial provides examples and practical guidance. @@ -43,4 +48,3 @@ If you have questions, feedback or run into any issues, we’re happy to help. You can also join the Membrane discussions on GitHub: https://github.com/membrane/api-gateway/discussions - diff --git a/distribution/tutorials/ai/mcp/10-MCP-Server.yaml b/distribution/tutorials/ai/mcp/10-MCP-Server.yaml new file mode 100644 index 0000000000..d7261ac844 --- /dev/null +++ b/distribution/tutorials/ai/mcp/10-MCP-Server.yaml @@ -0,0 +1,54 @@ +# yaml-language-server: $schema=https://www.membrane-api.io/v7.2.1.json +# +# Tutorial: MCP Server +# +# This configuration exposes the Membrane MCP server directly on +# http://localhost:2000 and starts two demo APIs that generate traffic +# the AI client can inspect through MCP. +# +# 1.) Start Membrane: +# +# Linux/Mac: +# ./membrane.sh -c 10-MCP-Server.yaml +# Windows: +# membrane.cmd -c 10-MCP-Server.yaml +# +# 2.) Generate some demo traffic in a second terminal: +# +# Linux/Mac: +# ./generate-traffic.sh 20 +# Windows: +# generate-traffic.cmd 20 +# +# The script also sends a few typical WordPress/PHP probe requests. +# +# 3.) Configure Claude Desktop as described in README.md. +# +# 4.) Ask the AI client to inspect recent exchanges or list the available APIs. +# Example prompts: +# - List the available APIs exposed by this Membrane MCP server. +# - Show me the most recent exchanges and summarize what happened. +# - Which endpoints received the most requests in the recent traffic? + +api: + port: 2000 + name: MCP-Server + flow: + - membraneMCPServer: + maxExchanges: 10000 + +--- +api: + port: 3000 + name: Fruitshop + path: + uri: /shop/v2/ + target: + url: https://api.predic8.de + +--- +api: + port: 3001 + name: ApiBin + target: + url: https://apibin.io/ diff --git a/distribution/tutorials/ai/mcp/README.md b/distribution/tutorials/ai/mcp/README.md new file mode 100644 index 0000000000..bf606aaf2b --- /dev/null +++ b/distribution/tutorials/ai/mcp/README.md @@ -0,0 +1,37 @@ +# Membrane API Gateway Tutorial - MCP + +This tutorial shows how to expose Membrane API Gateway as an MCP server for AI clients. +It covers: + +- running a local MCP server +- inspecting recent API traffic through MCP + +To begin, open [10-MCP-Server.yaml](10-MCP-Server.yaml) and follow the instructions in the file. + +If you want to protect the MCP endpoint, put the `membraneMCPServer` behind an `apiKey` interceptor and expose it only through the network path you actually want clients to use. If the client should keep talking to a simple local URL, you can also place a small local proxy in front of the protected MCP endpoint and let that proxy add the required authentication details. + +## Claude Desktop Setup + +Open `Settings` -> `Developer`, edit `claude_desktop_config.json`, and add: + +```json +{ + "mcpServers": { + "membrane": { + "command": "npx", + "args": [ + "mcp-remote", + "http://localhost:2000" + ] + } + }, + "preferences": { + "coworkScheduledTasksEnabled": true, + "ccdScheduledTasksEnabled": true, + "sidebarMode": "task", + "coworkWebSearchEnabled": true + } +} +``` + +Start membrane and restart Claude Desktop. diff --git a/distribution/tutorials/ai/mcp/generate-traffic.cmd b/distribution/tutorials/ai/mcp/generate-traffic.cmd new file mode 100644 index 0000000000..b8a61f7274 --- /dev/null +++ b/distribution/tutorials/ai/mcp/generate-traffic.cmd @@ -0,0 +1,44 @@ +@echo off +setlocal EnableExtensions EnableDelayedExpansion + +if "%FRUIT_BASE%"=="" set FRUIT_BASE=http://localhost:3000/shop/v2 +if "%APIBIN_BASE%"=="" set APIBIN_BASE=http://localhost:3001 +if "%ATTACK_BASE%"=="" set ATTACK_BASE=http://localhost:3000 + +if "%~1"=="" ( + set ROUNDS=20 +) else ( + set ROUNDS=%~1 +) + +for /L %%i in (1,1,%ROUNDS%) do ( + call :request GET "%FRUIT_BASE%/products/" + call :request GET "%FRUIT_BASE%/products/4" + call :request GET "%FRUIT_BASE%/categories/" + + call :request GET "%APIBIN_BASE%/analyze?round=%%i&delay=50" + call :request GET "%APIBIN_BASE%/faker?profile=order&count=2&locale=de-DE&seed=%%i" + call :request POST "%APIBIN_BASE%/echo" "hello apibin round %%i" + call :request GET "%ATTACK_BASE%/wp-login.php" + call :request GET "%ATTACK_BASE%/xmlrpc.php" + call :request GET "%ATTACK_BASE%/wp-admin/install.php" + call :request GET "%ATTACK_BASE%/.env" + call :request GET "%ATTACK_BASE%/phpinfo.php" +) + +exit /b 0 + +:request +set METHOD=%~1 +set URL=%~2 +set BODY=%~3 + +echo %METHOD% %URL% + +if "%BODY%"=="" ( + curl -sS -o NUL -w " -^> %%{http_code}\n" -X %METHOD% "%URL%" +) else ( + curl -sS -o NUL -w " -^> %%{http_code}\n" -X %METHOD% "%URL%" -H "Content-Type: text/plain" --data "%BODY%" +) + +exit /b 0 diff --git a/distribution/tutorials/ai/mcp/generate-traffic.sh b/distribution/tutorials/ai/mcp/generate-traffic.sh new file mode 100644 index 0000000000..58d5805958 --- /dev/null +++ b/distribution/tutorials/ai/mcp/generate-traffic.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env sh +set -eu + +FRUIT_BASE="${FRUIT_BASE:-http://localhost:3000/shop/v2}" +APIBIN_BASE="${APIBIN_BASE:-http://localhost:3001}" +ATTACK_BASE="${ATTACK_BASE:-http://localhost:3000}" +ROUNDS="${1:-20}" + +request() { + method="$1" + url="$2" + body="${3:-}" + + printf "%s %s" "$method" "$url" + + if [ -n "$body" ]; then + curl -sS -o /dev/null -w " -> %{http_code}\n" \ + -X "$method" "$url" \ + -H "Content-Type: text/plain" \ + --data "$body" || true + else + curl -sS -o /dev/null -w " -> %{http_code}\n" \ + -X "$method" "$url" || true + fi +} + +i=1 +while [ "$i" -le "$ROUNDS" ]; do + request GET "$FRUIT_BASE/products/" + request GET "$FRUIT_BASE/products/4" + request GET "$FRUIT_BASE/categories/" + + request GET "$APIBIN_BASE/analyze?round=$i&delay=50" + request GET "$APIBIN_BASE/faker?profile=order&count=2&locale=de-DE&seed=$i" + request POST "$APIBIN_BASE/echo" "hello apibin round $i" + request GET "$ATTACK_BASE/wp-login.php" + request GET "$ATTACK_BASE/xmlrpc.php" + request GET "$ATTACK_BASE/wp-admin/install.php" + request GET "$ATTACK_BASE/.env" + request GET "$ATTACK_BASE/phpinfo.php" + + i=$((i + 1)) +done diff --git a/distribution/tutorials/ai/mcp/membrane.cmd b/distribution/tutorials/ai/mcp/membrane.cmd new file mode 100644 index 0000000000..8d2d64e9cf --- /dev/null +++ b/distribution/tutorials/ai/mcp/membrane.cmd @@ -0,0 +1,24 @@ +@echo off +setlocal EnableExtensions + +set "SCRIPT_DIR=%~dp0" +if "%SCRIPT_DIR:~-1%"=="\" set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%" + +set "dir=%SCRIPT_DIR%" + +:search_up +if exist "%dir%\LICENSE.txt" if exist "%dir%\scripts\run-membrane.cmd" goto found +for %%A in ("%dir%\..") do set "next=%%~fA" +if /I "%next%"=="%dir%" goto notfound +set "dir=%next%" +goto search_up + +:found +set "MEMBRANE_HOME=%dir%" +set "MEMBRANE_CALLER_DIR=%SCRIPT_DIR%" +call "%MEMBRANE_HOME%\scripts\run-membrane.cmd" %* +exit /b %ERRORLEVEL% + +:notfound +>&2 echo Could not locate Membrane root. Ensure directory structure is correct. +exit /b 1 diff --git a/distribution/tutorials/ai/mcp/membrane.sh b/distribution/tutorials/ai/mcp/membrane.sh new file mode 100755 index 0000000000..b472b6cca5 --- /dev/null +++ b/distribution/tutorials/ai/mcp/membrane.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# Default: ./proxies.xml (next to this script); fallback -> $MEMBRANE_HOME/conf/proxies.xml +# JAVA_OPTS: relative -D paths are auto-resolved against $MEMBRANE_HOME (absolute/URI unchanged). +# Examples: +# export JAVA_OPTS='-Dlog4j.configurationFile=examples/logging/access/log4j2_access.xml' +# export JAVA_OPTS='-Dlog4j.configurationFile=/abs/path/log4j2.xml' + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) + +dir="$SCRIPT_DIR" +while [ "$dir" != "/" ]; do + if [ -f "$dir/LICENSE.txt" ] && [ -f "$dir/scripts/run-membrane.sh" ]; then + export MEMBRANE_HOME="$dir" + export MEMBRANE_CALLER_DIR="$SCRIPT_DIR" + exec sh "$dir/scripts/run-membrane.sh" "$@" + fi + dir=$(dirname "$dir") +done + +echo "Could not locate Membrane root. Ensure directory structure is correct." >&2 +exit 1 diff --git a/distribution/tutorials/ai/mcp/run-docker.cmd b/distribution/tutorials/ai/mcp/run-docker.cmd new file mode 100644 index 0000000000..844911377d --- /dev/null +++ b/distribution/tutorials/ai/mcp/run-docker.cmd @@ -0,0 +1,15 @@ +@echo off +setlocal enabledelayedexpansion + +set "DIR=%~dp0" +set "IMAGE=predic8/membrane:7.2.1" + +for /f "delims=" %%i in ('docker create -p 2000-2010:2000-2010 %IMAGE% %*') do set "CID=%%i" + +set "CLEANUP_CMD=docker rm -f %CID% >nul 2>nul" + +docker cp "%DIR%." "%CID%:/opt/membrane/" >nul +docker start -a "%CID%" + +%CLEANUP_CMD% +endlocal diff --git a/distribution/tutorials/ai/mcp/run-docker.sh b/distribution/tutorials/ai/mcp/run-docker.sh new file mode 100755 index 0000000000..9d342e8614 --- /dev/null +++ b/distribution/tutorials/ai/mcp/run-docker.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +cid="$(docker create -it -p 2000-2010:2000-2010 predic8/membrane:7.2.1 "$@")" + +cleanup() { + docker rm -f "$cid" >/dev/null 2>&1 || true +} +trap cleanup EXIT INT TERM + +docker cp "${DIR}/." "${cid}:/opt/membrane/" +docker start -a "$cid"