|
| 1 | + |
| 2 | +# MCP-MitM |
| 3 | + |
| 4 | +This is a man-in-the-middle that facades to MCP tools of any MCP HTTPStreamable server, and does some cool OIDC to APIKey stuff. |
| 5 | + |
| 6 | +# Develop and debug locally |
| 7 | + |
| 8 | +To use it, `go run cli/main/main.go`. Most secured MCP servers expect authorization to get a list of tools, and this |
| 9 | +is provided with `$INIT_AUTH_HEADER`. |
| 10 | + |
| 11 | +The code can be tested locally. For example, to facade the public GitHub MCP server, |
| 12 | + |
| 13 | +```bash |
| 14 | +UPSTREAM_MCP="https://api.githubcopilot.com/mcp/" \ |
| 15 | +INIT_AUTH_HEADER="Bearer ${GITHUB_TOKEN}" \ |
| 16 | +REQUIRED_SCOPE=profile \ |
| 17 | +UPSTREAM_HEADER_TO_USE_IF_IN_AUDIENCE="Bearer $GITHUB_TOKEN" \ |
| 18 | +UPSTREAM_HEADER_TO_USE_IF_NOT_IN_AUDIENCE="Bearer rutabaga" \ |
| 19 | +go run cli/main/main.go |
| 20 | +``` |
| 21 | + |
| 22 | +First, get a `GITHUB_TOKEN` by creating one at https://github.com/settings/tokens |
| 23 | + |
| 24 | +At this point, the server localhost:9090 will be offering the same tools as the upstream. |
| 25 | + |
| 26 | +To use it, we must first follow the standard MCP HTTPSTreamable init dance: |
| 27 | + |
| 28 | +```bash |
| 29 | +MCP=http://localhost:9090/mcp |
| 30 | +curl --include -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" ${MCP} --data ' |
| 31 | +{ |
| 32 | + "jsonrpc": "2.0", |
| 33 | + "id": 1, |
| 34 | + "method": "initialize", |
| 35 | + "params": { |
| 36 | + "protocolVersion": "2025-03-26", |
| 37 | + "capabilities": { |
| 38 | + "roots": { |
| 39 | + "listChanged": true |
| 40 | + }, |
| 41 | + "sampling": {} |
| 42 | + }, |
| 43 | + "clientInfo": { |
| 44 | + "name": "ExampleClient", |
| 45 | + "version": "1.0.0" |
| 46 | + } |
| 47 | + } |
| 48 | +} |
| 49 | +' | tee /tmp/init-response.txt |
| 50 | + |
| 51 | +SESSION_ID=$(cat /tmp/init-response.txt | grep -i mcp-session-id: | sed 's/mcp-session-id: //I' | sed 's/\r//g') |
| 52 | +echo SESSION_ID is "${SESSION_ID}" |
| 53 | + |
| 54 | +curl -v -X POST -H "mcp-session-id: ${SESSION_ID}" -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" ${MCP} --data ' |
| 55 | +{ |
| 56 | + "jsonrpc": "2.0", |
| 57 | + "method": "notifications/initialized" |
| 58 | +} |
| 59 | +' |
| 60 | +``` |
| 61 | + |
| 62 | +At this point, optionally, you can ask for tools to verify everything worked: |
| 63 | + |
| 64 | +```bash |
| 65 | +curl -v ${MCP} -X POST -H "mcp-session-id: ${SESSION_ID}" -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" --data ' |
| 66 | +{ |
| 67 | + "jsonrpc": "2.0", |
| 68 | + "id": 1, |
| 69 | + "method": "tools/list" |
| 70 | +} |
| 71 | +' | jq |
| 72 | +``` |
| 73 | + |
| 74 | +Now we want to invoke a tool, to prove it works. This facade expects an OIDC token. We'll need one. To get one from Keycloak in a Kagenti environment: |
| 75 | + |
| 76 | +```bash |
| 77 | +KEYCLOAK_USER=admin |
| 78 | +KEYCLOAK_PASSWORD=... # use yours |
| 79 | +KAGENTI_SECRET=$(oc -n kagenti-system get secret kagenti-ui-oauth-secret -o jsonpath="{.data.CLIENT_SECRET}" | base64 -d) |
| 80 | +MITM_TOKEN=$(curl -sX POST -H "Content-Type: application/x-www-form-urlencoded" \ |
| 81 | + -d "client_secret=$KAGENTI_SECRET" -d "client_id=kagenti" \ |
| 82 | + -d "grant_type=password" \ |
| 83 | + -d "username=${KEYCLOAK_USER}" -d "password=${KEYCLOAK_PASSWORD}" \ |
| 84 | + "http://keycloak.localtest.me:8080/realms/master/protocol/openid-connect/token" | jq --raw-output ".access_token") |
| 85 | +echo '$MITM_TOKEN' is ${MITM_TOKEN} |
| 86 | +MITM_AUTH="Bearer ${MITM_TOKEN}" |
| 87 | +``` |
| 88 | + |
| 89 | +REQUIRED_SCOPE tells the man-in-the-middle the expected scope to find. |
| 90 | + |
| 91 | +```bash |
| 92 | +export REQUIRED_SCOPE=profile |
| 93 | +export UPSTREAM_HEADER_TO_USE_IF_IN_AUDIENCE="Bearer $GITHUB_TOKEN" |
| 94 | +export UPSTREAM_HEADER_TO_USE_IF_NOT_IN_AUDIENCE="Bearer rutabaga" |
| 95 | +``` |
| 96 | + |
| 97 | +To test, we can invoke the man-in-the-middle MCP server with the token from Keycloak simulating how an OIDC-secured Agent |
| 98 | +would invoke it: |
| 99 | + |
| 100 | +```bash |
| 101 | +curl -v ${MCP} -H "Authorization: ${MITM_AUTH}" -H "mcp-session-id: ${SESSION_ID}" -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" --data ' |
| 102 | +{ |
| 103 | + "jsonrpc": "2.0", |
| 104 | + "id": 2, |
| 105 | + "method": "tools/call", |
| 106 | + "params": { |
| 107 | + "name": "list_branches", |
| 108 | + "arguments": { |
| 109 | + "owner": "kagenti", |
| 110 | + "repo": "kagenti" |
| 111 | + } |
| 112 | + } |
| 113 | +} |
| 114 | +' | jq |
| 115 | +``` |
| 116 | + |
| 117 | +If this works, you should see the list of tools, as JSON. |
| 118 | + |
| 119 | +# Deploying in Kagenti |
| 120 | + |
| 121 | +First, you'll need to give Kagenti credentials. For the GitHub OAuth API key demo, you'll need to supply API keys |
| 122 | + |
| 123 | +oc -n team1 get configmap environments -o yaml > /tmp/environments.yaml |
| 124 | +(edit /tmp/environments.yaml) creating the following, and replacing `${GITHUB_TOKEN}` with your GitHub personal access token (PAT): |
| 125 | + |
| 126 | +```YAML |
| 127 | +data: |
| 128 | + UPSTREAM_MCP: https://api.githubcopilot.com/mcp/ |
| 129 | + REQUIRED_SCOPE: profile |
| 130 | + INIT_AUTH_HEADER: Bearer ${GITHUB_TOKEN} |
| 131 | + UPSTREAM_HEADER_TO_USE_IF_IN_AUDIENCE: Bearer $GITHUB_TOKEN |
| 132 | + UPSTREAM_HEADER_TO_USE_IF_NOT_IN_AUDIENCE: Bearer rutabaga |
| 133 | +``` |
| 134 | +
|
| 135 | +After adding the new env vars, apply to Kagenti using `kubectl apply -n team1 -f /tmp/environments.yaml`. |
| 136 | + |
| 137 | +Now that the environment variables are available, start an instance of the tool |
| 138 | + |
| 139 | +- Browse to http://kagenti-ui.localtest.me:8080/Import_New_Tool |
| 140 | +- Select namespace (e.g. `team1`) |
| 141 | +- Set the Target Port to 9090 |
| 142 | +- Specify Subfolder `mcp/github_tool` |
| 143 | +- Click "Build & Deploy New Tool" to deploy. |
| 144 | + |
| 145 | +Once the tool is deployed, if you wish to test it from outside Kagenti you'll need to create an HTTPRoute for it. |
| 146 | + |
| 147 | +```bash |
| 148 | +cat <<EOF | > /tmp/expose-github-tool.yaml |
| 149 | +apiVersion: gateway.networking.k8s.io/v1 |
| 150 | +kind: HTTPRoute |
| 151 | +metadata: |
| 152 | + labels: |
| 153 | + mcp-server: "true" |
| 154 | + name: github-route-ext |
| 155 | + namespace: team1 |
| 156 | +spec: |
| 157 | + hostnames: |
| 158 | + - github-tool.127-0-0-1.sslip.io |
| 159 | + parentRefs: |
| 160 | + - group: gateway.networking.k8s.io |
| 161 | + kind: Gateway |
| 162 | + name: mcp-gateway |
| 163 | + namespace: gateway-system |
| 164 | + rules: |
| 165 | + - backendRefs: |
| 166 | + - group: "" |
| 167 | + kind: Service |
| 168 | + name: github-tool |
| 169 | + port: 8000 |
| 170 | + weight: 1 |
| 171 | + matches: |
| 172 | + - path: |
| 173 | + type: PathPrefix |
| 174 | + value: / |
| 175 | +EOF |
| 176 | +oc --context kind-agent-platform apply -f /tmp/expose-github-tool.yaml |
| 177 | +``` |
| 178 | + |
| 179 | +At this point, you can do the MCP curls above if you define `MCP=http://github-tool.127-0-0-1.sslip.io:8888/mcp` |
| 180 | + |
0 commit comments