From ddf12343fb430242b310bacea45e58e40b5481b9 Mon Sep 17 00:00:00 2001 From: Hai Huang Date: Mon, 4 May 2026 12:18:54 -0400 Subject: [PATCH 1/5] refactor(mcp-parser): Generic method handling and improved logging Remove hardcoded method switch (tools/call, resources/read, prompts/get) in favor of a generic approach that handles any MCP JSON-RPC method. All methods are logged uniformly at Info level; payload is logged at Debug level (truncated to 128 chars). MCPExtension now stores raw Params instead of method-specific sub-structs, making the parser forward-compatible with future MCP protocol changes. Assisted-By: Claude (Anthropic AI) Signed-off-by: Hai Huang --- authbridge/authlib/pipeline/extensions.go | 27 ++-------- authbridge/authlib/plugins/mcpparser.go | 42 +++------------ authbridge/authlib/plugins/mcpparser_test.go | 56 +++++++++----------- 3 files changed, 37 insertions(+), 88 deletions(-) diff --git a/authbridge/authlib/pipeline/extensions.go b/authbridge/authlib/pipeline/extensions.go index 627cc2e90..f6445b5df 100644 --- a/authbridge/authlib/pipeline/extensions.go +++ b/authbridge/authlib/pipeline/extensions.go @@ -13,31 +13,10 @@ type Extensions struct { } // MCPExtension carries parsed MCP JSON-RPC metadata. -// Exactly one of Tool, Resource, or Prompt is populated per request. type MCPExtension struct { - Method string // JSON-RPC method: "tools/call", "resources/read", "prompts/get" - RPCID any // JSON-RPC id for request-response correlation - - Tool *MCPToolMetadata - Resource *MCPResourceMetadata - Prompt *MCPPromptMetadata -} - -// MCPToolMetadata is populated for tools/call requests. -type MCPToolMetadata struct { - Name string - Args map[string]any -} - -// MCPResourceMetadata is populated for resources/read requests. -type MCPResourceMetadata struct { - URI string -} - -// MCPPromptMetadata is populated for prompts/get requests. -type MCPPromptMetadata struct { - Name string - Args map[string]string + Method string // JSON-RPC method (e.g. "tools/call", "resources/read", "initialize") + RPCID any // JSON-RPC id for request-response correlation + Params map[string]any // raw params from the JSON-RPC request } // A2AExtension carries parsed A2A protocol metadata. diff --git a/authbridge/authlib/plugins/mcpparser.go b/authbridge/authlib/plugins/mcpparser.go index d3c566efe..578564068 100644 --- a/authbridge/authlib/plugins/mcpparser.go +++ b/authbridge/authlib/plugins/mcpparser.go @@ -36,34 +36,15 @@ func (p *MCPParser) OnRequest(_ context.Context, pctx *pipeline.Context) pipelin return pipeline.Action{Type: pipeline.Continue} } - ext := &pipeline.MCPExtension{ + pctx.Extensions.MCP = &pipeline.MCPExtension{ Method: rpc.Method, RPCID: rpc.ID, + Params: rpc.Params, } - switch rpc.Method { - case "tools/call": - ext.Tool = &pipeline.MCPToolMetadata{ - Name: rpc.stringParam("name"), - Args: rpc.mapParam("arguments"), - } - slog.Info("mcp-parser: parsed tools/call", "tool", ext.Tool.Name) - case "resources/read": - ext.Resource = &pipeline.MCPResourceMetadata{ - URI: rpc.stringParam("uri"), - } - slog.Info("mcp-parser: parsed resources/read", "uri", ext.Resource.URI) - case "prompts/get": - ext.Prompt = &pipeline.MCPPromptMetadata{ - Name: rpc.stringParam("name"), - Args: rpc.stringMapParam("arguments"), - } - slog.Info("mcp-parser: parsed prompts/get", "prompt", ext.Prompt.Name) - default: - slog.Debug("mcp-parser: untracked method", "method", rpc.Method) - } + slog.Info("mcp-parser", "method", rpc.Method) + slog.Debug("mcp-parser: payload", "method", rpc.Method, "body", truncate(string(pctx.Body), 128)) - pctx.Extensions.MCP = ext return pipeline.Action{Type: pipeline.Continue} } @@ -91,16 +72,9 @@ func (r *jsonRPCRequest) mapParam(key string) map[string]any { return nil } -func (r *jsonRPCRequest) stringMapParam(key string) map[string]string { - raw, ok := r.Params[key].(map[string]any) - if !ok { - return nil - } - out := make(map[string]string, len(raw)) - for k, v := range raw { - if s, ok := v.(string); ok { - out[k] = s - } +func truncate(s string, max int) string { + if len(s) <= max { + return s } - return out + return s[:max] + "..." } diff --git a/authbridge/authlib/plugins/mcpparser_test.go b/authbridge/authlib/plugins/mcpparser_test.go index 4cd04dd8b..d16d159d0 100644 --- a/authbridge/authlib/plugins/mcpparser_test.go +++ b/authbridge/authlib/plugins/mcpparser_test.go @@ -42,14 +42,15 @@ func TestMCPParser_ToolCall(t *testing.T) { if pctx.Extensions.MCP.RPCID != float64(1) { t.Errorf("RPCID = %v, want 1", pctx.Extensions.MCP.RPCID) } - if pctx.Extensions.MCP.Tool == nil { - t.Fatal("Tool is nil") + if pctx.Extensions.MCP.Params["name"] != "get_weather" { + t.Errorf("Params[name] = %v, want %q", pctx.Extensions.MCP.Params["name"], "get_weather") } - if pctx.Extensions.MCP.Tool.Name != "get_weather" { - t.Errorf("Tool.Name = %q, want %q", pctx.Extensions.MCP.Tool.Name, "get_weather") + args, ok := pctx.Extensions.MCP.Params["arguments"].(map[string]any) + if !ok { + t.Fatal("Params[arguments] is not a map") } - if pctx.Extensions.MCP.Tool.Args["city"] != "NYC" { - t.Errorf("Tool.Args[city] = %v, want %q", pctx.Extensions.MCP.Tool.Args["city"], "NYC") + if args["city"] != "NYC" { + t.Errorf("Params[arguments][city] = %v, want %q", args["city"], "NYC") } } @@ -66,11 +67,11 @@ func TestMCPParser_ResourceRead(t *testing.T) { if pctx.Extensions.MCP == nil { t.Fatal("Extensions.MCP is nil") } - if pctx.Extensions.MCP.Resource == nil { - t.Fatal("Resource is nil") + if pctx.Extensions.MCP.Method != "resources/read" { + t.Errorf("Method = %q, want %q", pctx.Extensions.MCP.Method, "resources/read") } - if pctx.Extensions.MCP.Resource.URI != "file:///tmp/data.csv" { - t.Errorf("Resource.URI = %q, want %q", pctx.Extensions.MCP.Resource.URI, "file:///tmp/data.csv") + if pctx.Extensions.MCP.Params["uri"] != "file:///tmp/data.csv" { + t.Errorf("Params[uri] = %v, want %q", pctx.Extensions.MCP.Params["uri"], "file:///tmp/data.csv") } } @@ -87,18 +88,22 @@ func TestMCPParser_PromptGet(t *testing.T) { if pctx.Extensions.MCP == nil { t.Fatal("Extensions.MCP is nil") } - if pctx.Extensions.MCP.Prompt == nil { - t.Fatal("Prompt is nil") + if pctx.Extensions.MCP.Method != "prompts/get" { + t.Errorf("Method = %q, want %q", pctx.Extensions.MCP.Method, "prompts/get") } - if pctx.Extensions.MCP.Prompt.Name != "summarize" { - t.Errorf("Prompt.Name = %q, want %q", pctx.Extensions.MCP.Prompt.Name, "summarize") + if pctx.Extensions.MCP.Params["name"] != "summarize" { + t.Errorf("Params[name] = %v, want %q", pctx.Extensions.MCP.Params["name"], "summarize") } - if pctx.Extensions.MCP.Prompt.Args["style"] != "brief" { - t.Errorf("Prompt.Args[style] = %q, want %q", pctx.Extensions.MCP.Prompt.Args["style"], "brief") + args, ok := pctx.Extensions.MCP.Params["arguments"].(map[string]any) + if !ok { + t.Fatal("Params[arguments] is not a map") + } + if args["style"] != "brief" { + t.Errorf("Params[arguments][style] = %v, want %q", args["style"], "brief") } } -func TestMCPParser_UnknownMethod(t *testing.T) { +func TestMCPParser_AnyMethod(t *testing.T) { p := NewMCPParser() pctx := &pipeline.Context{ Body: []byte(`{"jsonrpc":"2.0","method":"notifications/initialized","id":4}`), @@ -114,15 +119,6 @@ func TestMCPParser_UnknownMethod(t *testing.T) { if pctx.Extensions.MCP.Method != "notifications/initialized" { t.Errorf("Method = %q, want %q", pctx.Extensions.MCP.Method, "notifications/initialized") } - if pctx.Extensions.MCP.Tool != nil { - t.Error("Tool should be nil for unknown method") - } - if pctx.Extensions.MCP.Resource != nil { - t.Error("Resource should be nil for unknown method") - } - if pctx.Extensions.MCP.Prompt != nil { - t.Error("Prompt should be nil for unknown method") - } } func TestMCPParser_NilBody(t *testing.T) { @@ -177,11 +173,11 @@ func TestMCPParser_MissingParams(t *testing.T) { if pctx.Extensions.MCP == nil { t.Fatal("Extensions.MCP is nil") } - if pctx.Extensions.MCP.Tool == nil { - t.Fatal("Tool should not be nil for tools/call") + if pctx.Extensions.MCP.Method != "tools/call" { + t.Errorf("Method = %q, want %q", pctx.Extensions.MCP.Method, "tools/call") } - if pctx.Extensions.MCP.Tool.Name != "" { - t.Errorf("Tool.Name = %q, want empty", pctx.Extensions.MCP.Tool.Name) + if pctx.Extensions.MCP.Params != nil { + t.Errorf("Params = %v, want nil when params not present", pctx.Extensions.MCP.Params) } } From e9ccd6ab830363d8796b8f48ba34db8268ef2dc5 Mon Sep 17 00:00:00 2001 From: Hai Huang Date: Mon, 4 May 2026 12:34:22 -0400 Subject: [PATCH 2/5] style: Use consistent log key format in mcp-parser Assisted-By: Claude (Anthropic AI) Signed-off-by: Hai Huang --- authbridge/authlib/plugins/mcpparser.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authbridge/authlib/plugins/mcpparser.go b/authbridge/authlib/plugins/mcpparser.go index 578564068..796e8fd2a 100644 --- a/authbridge/authlib/plugins/mcpparser.go +++ b/authbridge/authlib/plugins/mcpparser.go @@ -42,7 +42,7 @@ func (p *MCPParser) OnRequest(_ context.Context, pctx *pipeline.Context) pipelin Params: rpc.Params, } - slog.Info("mcp-parser", "method", rpc.Method) + slog.Info("mcp-parser: request", "method", rpc.Method) slog.Debug("mcp-parser: payload", "method", rpc.Method, "body", truncate(string(pctx.Body), 128)) return pipeline.Action{Type: pipeline.Continue} From 62aa6a6e517d02da66671f587e42153eeb53a171 Mon Sep 17 00:00:00 2001 From: Hai Huang Date: Mon, 4 May 2026 12:50:17 -0400 Subject: [PATCH 3/5] docs: Fix SIGUSR1 log toggle instructions for minimal container The container image has no standalone kill/grep/cat/tr binaries. Replace broken instructions with a pure-bash-builtin one-liner that finds the authbridge PID dynamically via /proc/*/cmdline. Assisted-By: Claude (Anthropic AI) Signed-off-by: Hai Huang --- authbridge/cmd/authbridge/README.md | 5 ++++- authbridge/demos/mcp-parser/README.md | 16 +++------------- authbridge/demos/weather-agent/demo-ui.md | 16 ++++++---------- 3 files changed, 13 insertions(+), 24 deletions(-) diff --git a/authbridge/cmd/authbridge/README.md b/authbridge/cmd/authbridge/README.md index 69db58467..6acd366a1 100644 --- a/authbridge/cmd/authbridge/README.md +++ b/authbridge/cmd/authbridge/README.md @@ -183,7 +183,10 @@ LOG_LEVEL=debug authbridge --config /etc/authbridge/config.yaml Send `SIGUSR1` to toggle between `info` and `debug`: ```bash -kubectl exec deploy/weather-service -n team1 -c authbridge-proxy -- kill -USR1 1 +# The container has no standalone kill/grep binaries — use bash builtins only. +# Match on the full binary path to skip the entrypoint script (also at PID 1). +kubectl exec deploy/weather-service -n team1 -c envoy-proxy -- \ + bash -c 'for f in /proc/[0-9]*/cmdline; do [ -r "$f" ] || continue; c=$(<"$f"); [[ "$c" == /usr/local/bin/authbridge* ]] && kill -USR1 "${f//[!0-9]/}" && break; done' ``` Send again to toggle back. The current level is logged on each toggle. diff --git a/authbridge/demos/mcp-parser/README.md b/authbridge/demos/mcp-parser/README.md index 956bdacea..9e1d5f2c8 100644 --- a/authbridge/demos/mcp-parser/README.md +++ b/authbridge/demos/mcp-parser/README.md @@ -130,19 +130,9 @@ kubectl patch configmap authbridge-config -n team1 \ Or toggle at runtime without restart: ```bash -# Find the authbridge PID (not PID 1, which is entrypoint.sh in the -# combined envoy-proxy container): -ABPID=$(kubectl exec deploy/ -n team1 -c envoy-proxy -- \ - sh -c 'for f in /proc/[0-9]*/cmdline; do - if cat "$f" 2>/dev/null | tr "\0" " " | head -c 100 | \ - cat - /dev/null | sh -c "read x; case \$x in *authbridge*) exit 0;; *) exit 1;; esac"; then - echo "${f%/cmdline}" | tr -dc "0-9"; break - fi - done') -kubectl exec deploy/ -n team1 -c envoy-proxy -- kill -USR1 $ABPID - -# Or if you know the PID (typically 14 in the combined image): -kubectl exec deploy/ -n team1 -c envoy-proxy -- kill -USR1 14 +# The container has no standalone kill/grep — use bash builtins to find the PID. +kubectl exec deploy/ -n team1 -c envoy-proxy -- \ + bash -c 'for f in /proc/[0-9]*/cmdline; do [ -r "$f" ] || continue; c=$(<"$f"); [[ "$c" == /usr/local/bin/authbridge* ]] && kill -USR1 "${f//[!0-9]/}" && break; done' ``` ## Step 3: Restart the Agent Pod diff --git a/authbridge/demos/weather-agent/demo-ui.md b/authbridge/demos/weather-agent/demo-ui.md index 9ceafb9f1..8c746eba9 100644 --- a/authbridge/demos/weather-agent/demo-ui.md +++ b/authbridge/demos/weather-agent/demo-ui.md @@ -665,21 +665,17 @@ without redeploying. ### Toggle debug logging at runtime -Send `SIGUSR1` to the authbridge process using an ephemeral debug container: +Send `SIGUSR1` to the authbridge process. The container image is minimal (no +standalone `kill` or `grep` binaries), so use bash builtins to locate the PID: ```bash -POD=$(kubectl get pod -n team1 -l app.kubernetes.io/name=weather-service \ - --no-headers | grep Running | head -1 | awk '{print $1}') - # For envoy-sidecar mode: -kubectl debug -n team1 "$POD" \ - --image=ghcr.io/kagenti/kagenti-extensions/proxy-init:latest \ - --target=envoy-proxy -- kill -USR1 1 +kubectl exec deploy/weather-service -n team1 -c envoy-proxy -- \ + bash -c 'for f in /proc/[0-9]*/cmdline; do [ -r "$f" ] || continue; c=$(<"$f"); [[ "$c" == /usr/local/bin/authbridge* ]] && kill -USR1 "${f//[!0-9]/}" && break; done' # For proxy-sidecar mode: -kubectl debug -n team1 "$POD" \ - --image=ghcr.io/kagenti/kagenti-extensions/proxy-init:latest \ - --target=authbridge-proxy -- kill -USR1 1 +kubectl exec deploy/weather-service -n team1 -c authbridge-proxy -- \ + bash -c 'for f in /proc/[0-9]*/cmdline; do [ -r "$f" ] || continue; c=$(<"$f"); [[ "$c" == /usr/local/bin/authbridge* ]] && kill -USR1 "${f//[!0-9]/}" && break; done' ``` Send `SIGUSR1` again to toggle back to INFO level. From 0b202515413c513c893db337518f2bf92f74abeb Mon Sep 17 00:00:00 2001 From: Hai Huang Date: Mon, 4 May 2026 14:00:42 -0400 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20Address=20PR=20review=20feedback=20?= =?UTF-8?q?=E2=80=94=20stale=20doc=20refs=20and=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update README reference from .Tool.Name to .Params["name"] - Update MCPParser doc comment to reflect raw params design Assisted-By: Claude (Anthropic AI) Signed-off-by: Hai Huang --- authbridge/authlib/plugins/mcpparser.go | 4 ++-- authbridge/demos/mcp-parser/README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/authbridge/authlib/plugins/mcpparser.go b/authbridge/authlib/plugins/mcpparser.go index 796e8fd2a..81512d4e7 100644 --- a/authbridge/authlib/plugins/mcpparser.go +++ b/authbridge/authlib/plugins/mcpparser.go @@ -9,8 +9,8 @@ import ( ) // MCPParser parses MCP JSON-RPC 2.0 request bodies and populates -// pctx.Extensions.MCP with the parsed method, tool name, resource URI, -// or prompt name for downstream policy plugins. +// pctx.Extensions.MCP with the method, RPC ID, and raw params for +// downstream policy plugins. type MCPParser struct{} func NewMCPParser() *MCPParser { return &MCPParser{} } diff --git a/authbridge/demos/mcp-parser/README.md b/authbridge/demos/mcp-parser/README.md index 9e1d5f2c8..e33f3b814 100644 --- a/authbridge/demos/mcp-parser/README.md +++ b/authbridge/demos/mcp-parser/README.md @@ -275,7 +275,7 @@ You should see `allow_mode_override: true` for each ext_proc filter. With `pctx.Extensions.MCP` populated, future plugins can: - **tool-policy**: Allow/deny specific tools based on caller identity - (`pctx.Claims.Scopes` + `pctx.Extensions.MCP.Tool.Name`) + (`pctx.Claims.Scopes` + `pctx.Extensions.MCP.Params["name"]`) - **audit**: Log every tool invocation with full caller attribution - **guardrails**: Inspect tool arguments for PII or injection patterns - **rate-limit**: Per-tool rate limiting based on caller identity From 983dba287fafda5f73bc7728f4aad07bee4dd084 Mon Sep 17 00:00:00 2001 From: Hai Huang Date: Mon, 4 May 2026 14:02:38 -0400 Subject: [PATCH 5/5] refactor: Remove unused stringParam/mapParam helpers Dead code in this PR -- no callers remain after removing the method switch. The A2A parser (PR #363) will re-add them when it merges. Assisted-By: Claude (Anthropic AI) Signed-off-by: Hai Huang --- authbridge/authlib/plugins/mcpparser.go | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/authbridge/authlib/plugins/mcpparser.go b/authbridge/authlib/plugins/mcpparser.go index 81512d4e7..c792307eb 100644 --- a/authbridge/authlib/plugins/mcpparser.go +++ b/authbridge/authlib/plugins/mcpparser.go @@ -58,20 +58,6 @@ type jsonRPCRequest struct { Params map[string]any `json:"params"` } -func (r *jsonRPCRequest) stringParam(key string) string { - if v, ok := r.Params[key].(string); ok { - return v - } - return "" -} - -func (r *jsonRPCRequest) mapParam(key string) map[string]any { - if v, ok := r.Params[key].(map[string]any); ok { - return v - } - return nil -} - func truncate(s string, max int) string { if len(s) <= max { return s