Skip to content

Commit 6be02ff

Browse files
committed
feat(auth): Support manual PRM override
1 parent 9d8a4bc commit 6be02ff

File tree

5 files changed

+97
-1
lines changed

5 files changed

+97
-1
lines changed

cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ func NewCommand(opts *internal.ToolboxOptions) *cobra.Command {
124124
flags.BoolVar(&opts.Cfg.DisableReload, "disable-reload", false, "Disables dynamic reloading of tools file.")
125125
flags.BoolVar(&opts.Cfg.UI, "ui", false, "Launches the Toolbox UI web server.")
126126
flags.StringVar(&opts.Cfg.ToolboxUrl, "toolbox-url", "", "Specifies the Toolbox URL. Used as the resource field in the MCP PRM file when MCP Auth is enabled. Falls back to TOOLBOX_URL environment variable.")
127+
flags.StringVar(&opts.Cfg.McpPrmFile, "mcp-prm-file", "", "Path to a manual Protected Resource Metadata (PRM) JSON file. If provided, overrides auto-generation.")
127128
// TODO: Insecure by default. Might consider updating this for v1.0.0
128129
flags.StringSliceVar(&opts.Cfg.AllowedOrigins, "allowed-origins", []string{"*"}, "Specifies a list of origins permitted to access this server. Defaults to '*'.")
129130
flags.StringSliceVar(&opts.Cfg.AllowedHosts, "allowed-hosts", []string{"*"}, "Specifies a list of hosts permitted to access this server. Defaults to '*'.")

internal/server/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ type ServerConfig struct {
7272
UI bool
7373
// ToolboxUrl specifies the URL to advertise in the MCP PRM file as the resource field.
7474
ToolboxUrl string
75+
// McpPrmFile specifies the path to a manual Protected Resource Metadata (PRM) JSON file. If provided, overrides auto-generation.
76+
McpPrmFile string
7577
// Specifies a list of origins permitted to access this server.
7678
AllowedOrigins []string
7779
// Specifies a list of hosts permitted to access this server.

internal/server/mcp.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"fmt"
2424
"io"
2525
"net/http"
26+
"os"
2627
"sync"
2728
"time"
2829

@@ -305,7 +306,7 @@ func mcpRouter(s *Server) (chi.Router, error) {
305306
}
306307
}
307308

308-
if mcpAuthEnabled {
309+
if mcpAuthEnabled || s.mcpPrmFile != "" {
309310
r.Get("/.well-known/oauth-protected-resource", func(w http.ResponseWriter, r *http.Request) { prmHandler(s, w, r) })
310311
}
311312

@@ -689,6 +690,22 @@ type prmResponse struct {
689690

690691
// prmHandler generates the Protected Resource Metadata (PRM) file for MCP Authorization.
691692
func prmHandler(s *Server, w http.ResponseWriter, r *http.Request) {
693+
if s.mcpPrmFile != "" {
694+
prmBytes, err := os.ReadFile(s.mcpPrmFile)
695+
if err != nil {
696+
s.logger.ErrorContext(r.Context(), "failed to read manual PRM file", "error", err, "path", s.mcpPrmFile)
697+
// Returning 500 when it explicitly fails to read a configured file
698+
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
699+
return
700+
}
701+
w.Header().Set("Content-Type", "application/json")
702+
w.WriteHeader(http.StatusOK)
703+
if _, err := w.Write(prmBytes); err != nil {
704+
s.logger.ErrorContext(r.Context(), "failed to write manual PRM file response", "error", err)
705+
}
706+
return
707+
}
708+
692709
var servers []string
693710
var scopes []string
694711
for _, authSvc := range s.ResourceMgr.GetAuthServiceMap() {

internal/server/mcp_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,3 +1247,77 @@ func TestPRMEndpoint(t *testing.T) {
12471247
t.Errorf("unexpected PRM response: got %+v, want %+v", got, want)
12481248
}
12491249
}
1250+
1251+
func TestPRMEndpoint_ManualFile(t *testing.T) {
1252+
// Create a temporary manual PRM file
1253+
tmpFile, err := os.CreateTemp("", "manual_prm_*.json")
1254+
if err != nil {
1255+
t.Fatalf("failed to create temp file: %v", err)
1256+
}
1257+
defer os.Remove(tmpFile.Name())
1258+
1259+
manualPRMContent := []byte(`{
1260+
"resource": "https://manual.example.com/mcp",
1261+
"authorization_servers": ["https://manual-auth.example.com"],
1262+
"scopes_supported": ["manual:scope"],
1263+
"bearer_methods_supported": ["header"]
1264+
}`)
1265+
1266+
if _, err := tmpFile.Write(manualPRMContent); err != nil {
1267+
t.Fatalf("failed to write to temp file: %v", err)
1268+
}
1269+
tmpFile.Close()
1270+
1271+
// Initialize the server with the manual PRM file path
1272+
resourceManager := resources.NewResourceManager(nil, nil, nil, nil, nil, nil, nil)
1273+
testLogger, err := log.NewStdLogger(os.Stdout, os.Stderr, "info")
1274+
if err != nil {
1275+
t.Fatalf("unable to initialize logger: %s", err)
1276+
}
1277+
1278+
s := &Server{
1279+
logger: testLogger,
1280+
ResourceMgr: resourceManager,
1281+
mcpPrmFile: tmpFile.Name(), // Inject manual config path
1282+
}
1283+
1284+
r, err := mcpRouter(s)
1285+
if err != nil {
1286+
t.Fatalf("unexpected error creating router: %v", err)
1287+
}
1288+
1289+
ts := httptest.NewServer(r)
1290+
defer ts.Close()
1291+
1292+
// Make the request
1293+
resp, body, err := runRequest(ts, http.MethodGet, "/.well-known/oauth-protected-resource", nil, nil)
1294+
if err != nil {
1295+
t.Fatalf("unexpected error during request: %s", err)
1296+
}
1297+
1298+
if resp.StatusCode != http.StatusOK {
1299+
t.Fatalf("expected status %d, got %d", http.StatusOK, resp.StatusCode)
1300+
}
1301+
if contentType := resp.Header.Get("Content-Type"); contentType != "application/json" {
1302+
t.Fatalf("expected content-type application/json, got %s", contentType)
1303+
}
1304+
1305+
// Verify the response body matches the exact contents of the manual file
1306+
var got map[string]any
1307+
if err := json.Unmarshal(body, &got); err != nil {
1308+
t.Fatalf("unexpected error unmarshalling body: %s", err)
1309+
}
1310+
1311+
want := map[string]any{
1312+
"resource": "https://manual.example.com/mcp",
1313+
"authorization_servers": []any{
1314+
"https://manual-auth.example.com",
1315+
},
1316+
"scopes_supported": []any{"manual:scope"},
1317+
"bearer_methods_supported": []any{"header"},
1318+
}
1319+
1320+
if !reflect.DeepEqual(got, want) {
1321+
t.Errorf("unexpected manual PRM response: got %+v, want %+v", got, want)
1322+
}
1323+
}

internal/server/server.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ type Server struct {
5353
sseManager *sseManager
5454
ResourceMgr *resources.ResourceManager
5555
toolboxUrl string
56+
mcpPrmFile string
5657
}
5758

5859
func InitializeConfigs(ctx context.Context, cfg ServerConfig) (
@@ -380,6 +381,7 @@ func NewServer(ctx context.Context, cfg ServerConfig) (*Server, error) {
380381
sseManager: sseManager,
381382
ResourceMgr: resourceManager,
382383
toolboxUrl: cfg.ToolboxUrl,
384+
mcpPrmFile: cfg.McpPrmFile,
383385
}
384386

385387
// cors

0 commit comments

Comments
 (0)