Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 3 additions & 24 deletions authbridge/authlib/pipeline/extensions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
60 changes: 10 additions & 50 deletions authbridge/authlib/plugins/mcpparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{} }
Expand All @@ -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: request", "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}
}

Expand All @@ -77,30 +58,9 @@ 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 (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] + "..."
}
Comment thread
huang195 marked this conversation as resolved.
56 changes: 26 additions & 30 deletions authbridge/authlib/plugins/mcpparser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}

Expand All @@ -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")
}
}

Expand All @@ -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}`),
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
}
}

Expand Down
5 changes: 4 additions & 1 deletion authbridge/cmd/authbridge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
18 changes: 4 additions & 14 deletions authbridge/demos/mcp-parser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<agent-name> -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/<agent-name> -n team1 -c envoy-proxy -- kill -USR1 $ABPID

# Or if you know the PID (typically 14 in the combined image):
kubectl exec deploy/<agent-name> -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/<agent-name> -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
Expand Down Expand Up @@ -285,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
Expand Down
16 changes: 6 additions & 10 deletions authbridge/demos/weather-agent/demo-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading