-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.go
More file actions
211 lines (188 loc) · 6.4 KB
/
Copy pathmain.go
File metadata and controls
211 lines (188 loc) · 6.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
// mcp-authzen is a Model Context Protocol (MCP) server that fronts an
// OpenID AuthZEN 1.0 compliant Policy Decision Point (PDP). It exposes
// `authzen_evaluate` as an MCP tool so an LLM agent can ask "can subject S
// perform action A on resource R?" and route the question to a real PDP.
//
// The PDP endpoint can be configured via the AUTHZEN_PDP_URL environment
// variable or overridden per-call via the optional `pdp_url` argument.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
var version = "dev"
// authzenRequest matches the OpenID AuthZEN 1.0 Evaluation API request body.
//
// https://openid.net/specs/authorization-api-1_0.html
type authzenRequest struct {
Subject json.RawMessage `json:"subject"`
Resource json.RawMessage `json:"resource"`
Action json.RawMessage `json:"action"`
Context json.RawMessage `json:"context,omitempty"`
}
type authzenResponse struct {
Decision bool `json:"decision"`
Context json.RawMessage `json:"context,omitempty"`
}
func main() {
if len(os.Args) > 1 {
switch os.Args[1] {
case "--version", "-v", "version":
fmt.Printf("mcp-authzen %s\n", version)
return
case "--help", "-h", "help":
fmt.Println(`mcp-authzen — MCP server fronting an OpenID AuthZEN 1.0 PDP.
Usage:
mcp-authzen Run as an MCP stdio server.
mcp-authzen --version Print version.
Configuration:
AUTHZEN_PDP_URL Default PDP evaluation endpoint, e.g.
http://localhost:8181/access/v1/evaluation
Tools exposed:
authzen_evaluate POST to the PDP's evaluation endpoint with a
subject/resource/action/context bundle.`)
return
}
}
s := server.NewMCPServer(
"mcp-authzen",
version,
server.WithToolCapabilities(false),
)
s.AddTool(
mcp.NewTool("authzen_evaluate",
mcp.WithDescription(
"Ask an OpenID AuthZEN 1.0 PDP whether a subject is allowed "+
"to perform an action on a resource. Returns the PDP's "+
"decision (true/false) and optional context."),
mcp.WithString("subject",
mcp.Required(),
mcp.Description(`JSON object describing the principal. Per AuthZEN: `+
`{"type": "user", "id": "alice", "properties": {...}}.`),
),
mcp.WithString("resource",
mcp.Required(),
mcp.Description(`JSON object describing the target. Per AuthZEN: `+
`{"type": "document", "id": "doc-1", "properties": {...}}.`),
),
mcp.WithString("action",
mcp.Required(),
mcp.Description(`JSON object describing the action. Per AuthZEN: `+
`{"name": "read", "properties": {...}}.`),
),
mcp.WithString("context",
mcp.Description(`Optional JSON object with runtime context `+
`(IP, time, MFA strength, etc).`),
),
mcp.WithString("pdp_url",
mcp.Description(`Override the AUTHZEN_PDP_URL env. Must be a `+
`full URL to the evaluation endpoint.`),
),
),
evaluate,
)
if err := server.ServeStdio(s); err != nil {
log.Fatalf("mcp-authzen: %v", err)
}
}
var httpClient = &http.Client{Timeout: 10 * time.Second}
func evaluate(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
pdpURL := req.GetString("pdp_url", "")
if pdpURL == "" {
pdpURL = os.Getenv("AUTHZEN_PDP_URL")
}
if pdpURL == "" {
return mcp.NewToolResultError(
"no PDP URL: set AUTHZEN_PDP_URL or pass pdp_url"), nil
}
// Constrain the outbound target so a model-supplied `pdp_url` can't be used
// to scan internal addresses with non-HTTP schemes or omitted hosts.
if u, perr := url.Parse(pdpURL); perr != nil || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") {
return mcp.NewToolResultError("invalid pdp_url: must be an absolute http(s) URL with a host"), nil
}
subject, err := parseJSONArg(req, "subject", true)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
resource, err := parseJSONArg(req, "resource", true)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
action, err := parseJSONArg(req, "action", true)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
contextRaw, err := parseJSONArg(req, "context", false)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
body, _ := json.Marshal(authzenRequest{
Subject: subject,
Resource: resource,
Action: action,
Context: contextRaw,
})
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, pdpURL, bytes.NewReader(body))
if err != nil {
return mcp.NewToolResultError("build request: " + err.Error()), nil
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "application/json")
if token := os.Getenv("AUTHZEN_PDP_TOKEN"); token != "" {
if !strings.HasPrefix(token, "Bearer ") && !strings.HasPrefix(token, "Basic ") {
token = "Bearer " + token
}
httpReq.Header.Set("Authorization", token)
}
res, err := httpClient.Do(httpReq)
if err != nil {
return mcp.NewToolResultError("PDP request failed: " + err.Error()), nil
}
defer func() { _ = res.Body.Close() }()
// Cap the response at 1 MiB; AuthZEN responses are tiny and we don't want
// a misbehaving PDP to OOM the server.
raw, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
if err != nil {
return mcp.NewToolResultError("read PDP response: " + err.Error()), nil
}
if res.StatusCode >= 400 {
return mcp.NewToolResultError(fmt.Sprintf(
"PDP returned %d: %s", res.StatusCode, strings.TrimSpace(string(raw)))), nil
}
var decoded authzenResponse
if err := json.Unmarshal(raw, &decoded); err != nil {
return mcp.NewToolResultError("PDP response is not valid AuthZEN JSON: " + err.Error()), nil
}
out, _ := json.MarshalIndent(decoded, "", " ")
return mcp.NewToolResultText(string(out)), nil
}
func parseJSONArg(req mcp.CallToolRequest, name string, required bool) (json.RawMessage, error) {
s := req.GetString(name, "")
if s == "" {
if required {
return nil, fmt.Errorf("missing required arg %q", name)
}
return nil, nil
}
// AuthZEN entities (subject, resource, action, context) are all JSON
// objects — reject arrays / scalars early instead of letting the PDP do it.
var v any
if err := json.Unmarshal([]byte(s), &v); err != nil {
return nil, fmt.Errorf("arg %q is not valid JSON: %w", name, err)
}
if _, ok := v.(map[string]any); !ok {
return nil, fmt.Errorf("arg %q must be a JSON object", name)
}
return json.RawMessage(s), nil
}