mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat(coderd/mcp): user-headers GET/PUT/DELETE endpoints
Adds three experimental endpoints under /api/experimental/mcp/servers/
{mcpServer}/user-headers that let a user manage their own values for
the admin-marked custom_headers_user_keys on an MCP server.
- GET returns an MCPServerUserHeaderValues with a HasValues map keyed
by every admin-marked user key. Values are never returned, only
booleans indicating whether the caller has stored a non-empty value.
- PUT upserts a subset of values. Unknown keys are rejected with 400.
Keys are matched case-insensitively but stored under the admin's
canonical casing. A partial PUT preserves existing values for keys
the request did not touch; sending an empty string clears a single
value.
- DELETE removes the row entirely and is idempotent.
The Upsert dbauthz wrapper switches to insertWithAction so the first
write does not require a pre-existing row.
This commit is contained in:
@@ -1367,6 +1367,10 @@ func New(options *Options) *API {
|
||||
r.Get("/oauth2/connect", api.mcpServerOAuth2Connect)
|
||||
r.Get("/oauth2/callback", api.mcpServerOAuth2Callback)
|
||||
r.Delete("/oauth2/disconnect", api.mcpServerOAuth2Disconnect)
|
||||
// Per-user custom header values for admin-marked keys.
|
||||
r.Get("/user-headers", api.getMCPServerUserHeaderValues)
|
||||
r.Put("/user-headers", api.updateMCPServerUserHeaderValues)
|
||||
r.Delete("/user-headers", api.deleteMCPServerUserHeaderValues)
|
||||
})
|
||||
})
|
||||
// MCP HTTP transport endpoint with mandatory authentication
|
||||
|
||||
@@ -8277,13 +8277,7 @@ func (q *querier) UpsertLogoURL(ctx context.Context, value string) error {
|
||||
}
|
||||
|
||||
func (q *querier) UpsertMCPServerUserHeaderValues(ctx context.Context, arg database.UpsertMCPServerUserHeaderValuesParams) (database.McpServerUserHeaderValue, error) {
|
||||
fetch := func(ctx context.Context, arg database.UpsertMCPServerUserHeaderValuesParams) (database.McpServerUserHeaderValue, error) {
|
||||
return q.db.GetMCPServerUserHeaderValues(ctx, database.GetMCPServerUserHeaderValuesParams{
|
||||
MCPServerConfigID: arg.MCPServerConfigID,
|
||||
UserID: arg.UserID,
|
||||
})
|
||||
}
|
||||
return fetchAndQuery(q.log, q.auth, policy.ActionUpdatePersonal, fetch, q.db.UpsertMCPServerUserHeaderValues)(ctx, arg)
|
||||
return insertWithAction(q.log, q.auth, rbac.ResourceUser.WithID(arg.UserID).WithOwner(arg.UserID.String()), policy.ActionUpdatePersonal, q.db.UpsertMCPServerUserHeaderValues)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpsertMCPServerUserToken(ctx context.Context, arg database.UpsertMCPServerUserTokenParams) (database.MCPServerUserToken, error) {
|
||||
|
||||
@@ -1684,12 +1684,8 @@ func (s *MethodTestSuite) TestChats() {
|
||||
HeaderValues: `{"X-User-Token":"secret"}`,
|
||||
}
|
||||
value := testutil.Fake(s.T(), faker, database.McpServerUserHeaderValue{MCPServerConfigID: arg.MCPServerConfigID, UserID: arg.UserID})
|
||||
dbm.EXPECT().GetMCPServerUserHeaderValues(gomock.Any(), database.GetMCPServerUserHeaderValuesParams{
|
||||
MCPServerConfigID: arg.MCPServerConfigID,
|
||||
UserID: arg.UserID,
|
||||
}).Return(value, nil).AnyTimes()
|
||||
dbm.EXPECT().UpsertMCPServerUserHeaderValues(gomock.Any(), arg).Return(value, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(value, policy.ActionUpdatePersonal).Returns(value)
|
||||
check.Args(arg).Asserts(rbac.ResourceUser.WithID(arg.UserID).WithOwner(arg.UserID.String()), policy.ActionUpdatePersonal).Returns(value)
|
||||
}))
|
||||
s.Run("DeleteMCPServerUserHeaderValues", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
arg := database.DeleteMCPServerUserHeaderValuesParams{
|
||||
|
||||
+213
-2
@@ -1232,8 +1232,217 @@ func (api *API) mcpServerOAuth2Disconnect(rw http.ResponseWriter, r *http.Reques
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// parseMCPServerConfigID extracts the MCP server config UUID from the
|
||||
// "mcpServer" path parameter.
|
||||
// @Summary Get MCP user-set custom header values
|
||||
// @x-apidocgen {"skip": true}
|
||||
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
|
||||
//
|
||||
//nolint:revive // HTTP handler writes to ResponseWriter.
|
||||
func (api *API) getMCPServerUserHeaderValues(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
apiKey := httpmw.APIKey(r)
|
||||
|
||||
mcpServerID, ok := parseMCPServerConfigID(rw, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Load the config to know which keys the admin has marked as
|
||||
// user-set. We use system context because the user can't
|
||||
// authorize a direct ResourceDeploymentConfig read.
|
||||
//nolint:gocritic // Users read their own header values; need config metadata to bound the response.
|
||||
cfg, err := api.Database.GetMCPServerConfigByID(dbauthz.AsSystemRestricted(ctx), mcpServerID)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
if cfg.AuthType != "custom_headers" || len(cfg.CustomHeadersUserKeys) == 0 {
|
||||
// No user-set keys; respond with an empty has_values map.
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.MCPServerUserHeaderValues{
|
||||
MCPServerConfigID: cfg.ID,
|
||||
HasValues: map[string]bool{},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
row, err := api.Database.GetMCPServerUserHeaderValues(ctx, database.GetMCPServerUserHeaderValuesParams{
|
||||
MCPServerConfigID: mcpServerID,
|
||||
UserID: apiKey.UserID,
|
||||
})
|
||||
stored := map[string]string{}
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
if err == nil && strings.TrimSpace(row.HeaderValues) != "" {
|
||||
if decErr := json.Unmarshal([]byte(row.HeaderValues), &stored); decErr != nil {
|
||||
httpapi.InternalServerError(rw, xerrors.Errorf("decode header_values: %w", decErr))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
hasValues := make(map[string]bool, len(cfg.CustomHeadersUserKeys))
|
||||
for _, key := range cfg.CustomHeadersUserKeys {
|
||||
hasValues[key] = stored[key] != ""
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.MCPServerUserHeaderValues{
|
||||
MCPServerConfigID: cfg.ID,
|
||||
HasValues: hasValues,
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Update MCP user-set custom header values
|
||||
// @x-apidocgen {"skip": true}
|
||||
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
|
||||
func (api *API) updateMCPServerUserHeaderValues(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
apiKey := httpmw.APIKey(r)
|
||||
|
||||
mcpServerID, ok := parseMCPServerConfigID(rw, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var req codersdk.UpdateMCPServerUserHeaderValuesRequest
|
||||
if !httpapi.Read(ctx, rw, r, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
//nolint:gocritic // Users update their own header values; need config metadata to validate the request.
|
||||
cfg, err := api.Database.GetMCPServerConfigByID(dbauthz.AsSystemRestricted(ctx), mcpServerID)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
if cfg.AuthType != "custom_headers" {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "MCP server does not use custom_headers auth.",
|
||||
})
|
||||
return
|
||||
}
|
||||
if len(cfg.CustomHeadersUserKeys) == 0 {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "MCP server has no user-set custom header keys.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Build a case-insensitive lookup of allowed user keys, preserving
|
||||
// the admin's casing for storage.
|
||||
allowed := make(map[string]string, len(cfg.CustomHeadersUserKeys))
|
||||
for _, k := range cfg.CustomHeadersUserKeys {
|
||||
allowed[strings.ToLower(k)] = k
|
||||
}
|
||||
|
||||
// Validate every key in the request matches an allowed user key.
|
||||
normalized := make(map[string]string, len(req.Values))
|
||||
for reqKey, reqVal := range req.Values {
|
||||
canonical, ok := allowed[strings.ToLower(strings.TrimSpace(reqKey))]
|
||||
if !ok {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: fmt.Sprintf("Header %q is not in the MCP server's user-set custom header keys.", reqKey),
|
||||
})
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(reqVal) != "" {
|
||||
normalized[canonical] = reqVal
|
||||
}
|
||||
}
|
||||
|
||||
// Merge with any existing stored values so a partial update only
|
||||
// overwrites the keys it touches. A user can clear a single value
|
||||
// by sending an empty string for that key.
|
||||
merged := map[string]string{}
|
||||
existing, err := api.Database.GetMCPServerUserHeaderValues(ctx, database.GetMCPServerUserHeaderValuesParams{
|
||||
MCPServerConfigID: mcpServerID,
|
||||
UserID: apiKey.UserID,
|
||||
})
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
if err == nil && strings.TrimSpace(existing.HeaderValues) != "" {
|
||||
if decErr := json.Unmarshal([]byte(existing.HeaderValues), &merged); decErr != nil {
|
||||
httpapi.InternalServerError(rw, xerrors.Errorf("decode existing header_values: %w", decErr))
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, k := range cfg.CustomHeadersUserKeys {
|
||||
if _, sent := req.Values[k]; !sent {
|
||||
// Case-insensitive check for the canonical key.
|
||||
alreadyInRequest := false
|
||||
for reqKey := range req.Values {
|
||||
if strings.EqualFold(strings.TrimSpace(reqKey), k) {
|
||||
alreadyInRequest = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if alreadyInRequest {
|
||||
continue
|
||||
}
|
||||
// Preserve existing stored value if any.
|
||||
if v, has := merged[k]; has && v != "" {
|
||||
normalized[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
encoded, err := json.Marshal(normalized)
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, xerrors.Errorf("encode header_values: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.Database.UpsertMCPServerUserHeaderValues(ctx, database.UpsertMCPServerUserHeaderValuesParams{
|
||||
MCPServerConfigID: mcpServerID,
|
||||
UserID: apiKey.UserID,
|
||||
HeaderValues: string(encoded),
|
||||
HeaderValuesKeyID: sql.NullString{},
|
||||
}); err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
hasValues := make(map[string]bool, len(cfg.CustomHeadersUserKeys))
|
||||
for _, k := range cfg.CustomHeadersUserKeys {
|
||||
hasValues[k] = normalized[k] != ""
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.MCPServerUserHeaderValues{
|
||||
MCPServerConfigID: cfg.ID,
|
||||
HasValues: hasValues,
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Delete MCP user-set custom header values
|
||||
// @x-apidocgen {"skip": true}
|
||||
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
|
||||
func (api *API) deleteMCPServerUserHeaderValues(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
apiKey := httpmw.APIKey(r)
|
||||
|
||||
mcpServerID, ok := parseMCPServerConfigID(rw, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
err := api.Database.DeleteMCPServerUserHeaderValues(ctx, database.DeleteMCPServerUserHeaderValuesParams{
|
||||
MCPServerConfigID: mcpServerID,
|
||||
UserID: apiKey.UserID,
|
||||
})
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// refreshMCPUserToken attempts to refresh an expired OAuth2 token
|
||||
// for the given MCP server config. Returns true when the token is
|
||||
// valid (either still fresh or successfully refreshed), false when
|
||||
@@ -1295,6 +1504,8 @@ func (api *API) refreshMCPUserToken(
|
||||
return true
|
||||
}
|
||||
|
||||
// parseMCPServerConfigID extracts the MCP server config UUID from the
|
||||
// "mcpServer" path parameter.
|
||||
func parseMCPServerConfigID(rw http.ResponseWriter, r *http.Request) (uuid.UUID, bool) {
|
||||
mcpServerID, err := uuid.Parse(chi.URLParam(r, "mcpServer"))
|
||||
if err != nil {
|
||||
|
||||
@@ -2144,3 +2144,185 @@ func TestMCPOAuth2DiscoveryEdgeCases(t *testing.T) {
|
||||
require.True(t, created.HasOAuth2Secret)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMCPServerUserHeaderValuesEndpoints exercises the user-headers
|
||||
// GET/PUT/DELETE flow for the per-user custom header values.
|
||||
func TestMCPServerUserHeaderValuesEndpoints(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
createHonchoConfig := func(t *testing.T, client *codersdk.Client) codersdk.MCPServerConfig {
|
||||
t.Helper()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
cfg, err := client.CreateMCPServerConfig(ctx, codersdk.CreateMCPServerConfigRequest{
|
||||
DisplayName: "Honcho",
|
||||
Slug: "honcho",
|
||||
Transport: "streamable_http",
|
||||
URL: "https://mcp.example.com/v1",
|
||||
AuthType: "custom_headers",
|
||||
CustomHeaders: map[string]string{"X-Org-ID": "acme"},
|
||||
CustomHeadersUserKeys: []string{"X-User-Token", "X-Workspace"},
|
||||
Availability: "default_on",
|
||||
Enabled: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return cfg
|
||||
}
|
||||
|
||||
t.Run("GetReturnsAllFalseWhenNoValuesStored", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
client := newMCPClient(t)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
cfg := createHonchoConfig(t, client)
|
||||
|
||||
resp, err := client.MCPServerUserHeaderValues(ctx, cfg.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, cfg.ID, resp.MCPServerConfigID)
|
||||
require.False(t, resp.HasValues["X-User-Token"])
|
||||
require.False(t, resp.HasValues["X-Workspace"])
|
||||
})
|
||||
|
||||
t.Run("GetReturnsEmptyForNonCustomHeadersConfig", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
client := newMCPClient(t)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
cfg, err := client.CreateMCPServerConfig(ctx, codersdk.CreateMCPServerConfigRequest{
|
||||
DisplayName: "None",
|
||||
Slug: "none-cfg",
|
||||
Transport: "streamable_http",
|
||||
URL: "https://mcp.example.com/v1",
|
||||
AuthType: "none",
|
||||
Availability: "default_on",
|
||||
Enabled: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := client.MCPServerUserHeaderValues(ctx, cfg.ID)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, resp.HasValues)
|
||||
})
|
||||
|
||||
t.Run("PutThenGetReportsHasValuesTrue", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
client := newMCPClient(t)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
cfg := createHonchoConfig(t, client)
|
||||
|
||||
putResp, err := client.UpdateMCPServerUserHeaderValues(ctx, cfg.ID, codersdk.UpdateMCPServerUserHeaderValuesRequest{
|
||||
Values: map[string]string{"X-User-Token": "secret-jwt"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, putResp.HasValues["X-User-Token"])
|
||||
require.False(t, putResp.HasValues["X-Workspace"])
|
||||
|
||||
getResp, err := client.MCPServerUserHeaderValues(ctx, cfg.ID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, getResp.HasValues["X-User-Token"])
|
||||
require.False(t, getResp.HasValues["X-Workspace"])
|
||||
})
|
||||
|
||||
t.Run("PutRejectsUnknownKey", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
client := newMCPClient(t)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
cfg := createHonchoConfig(t, client)
|
||||
|
||||
_, err := client.UpdateMCPServerUserHeaderValues(ctx, cfg.ID, codersdk.UpdateMCPServerUserHeaderValuesRequest{
|
||||
Values: map[string]string{"X-Unknown": "oops"},
|
||||
})
|
||||
require.Error(t, err)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("PutPartialUpdatePreservesOtherKey", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
client := newMCPClient(t)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
cfg := createHonchoConfig(t, client)
|
||||
|
||||
_, err := client.UpdateMCPServerUserHeaderValues(ctx, cfg.ID, codersdk.UpdateMCPServerUserHeaderValuesRequest{
|
||||
Values: map[string]string{"X-User-Token": "jwt-a", "X-Workspace": "main"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Partial update touching only one key; the other must remain set.
|
||||
putResp, err := client.UpdateMCPServerUserHeaderValues(ctx, cfg.ID, codersdk.UpdateMCPServerUserHeaderValuesRequest{
|
||||
Values: map[string]string{"X-User-Token": "jwt-b"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, putResp.HasValues["X-User-Token"])
|
||||
require.True(t, putResp.HasValues["X-Workspace"])
|
||||
})
|
||||
|
||||
t.Run("PutClearsSingleValueWithEmptyString", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
client := newMCPClient(t)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
cfg := createHonchoConfig(t, client)
|
||||
|
||||
_, err := client.UpdateMCPServerUserHeaderValues(ctx, cfg.ID, codersdk.UpdateMCPServerUserHeaderValuesRequest{
|
||||
Values: map[string]string{"X-User-Token": "jwt-a"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
putResp, err := client.UpdateMCPServerUserHeaderValues(ctx, cfg.ID, codersdk.UpdateMCPServerUserHeaderValuesRequest{
|
||||
Values: map[string]string{"X-User-Token": ""},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.False(t, putResp.HasValues["X-User-Token"])
|
||||
})
|
||||
|
||||
t.Run("DeleteRemovesAllValues", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
client := newMCPClient(t)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
cfg := createHonchoConfig(t, client)
|
||||
|
||||
_, err := client.UpdateMCPServerUserHeaderValues(ctx, cfg.ID, codersdk.UpdateMCPServerUserHeaderValuesRequest{
|
||||
Values: map[string]string{"X-User-Token": "jwt-a", "X-Workspace": "main"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, client.DeleteMCPServerUserHeaderValues(ctx, cfg.ID))
|
||||
|
||||
getResp, err := client.MCPServerUserHeaderValues(ctx, cfg.ID)
|
||||
require.NoError(t, err)
|
||||
require.False(t, getResp.HasValues["X-User-Token"])
|
||||
require.False(t, getResp.HasValues["X-Workspace"])
|
||||
})
|
||||
|
||||
t.Run("DeleteIsIdempotent", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
client := newMCPClient(t)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
cfg := createHonchoConfig(t, client)
|
||||
|
||||
// Never set a value; delete should still return 204.
|
||||
require.NoError(t, client.DeleteMCPServerUserHeaderValues(ctx, cfg.ID))
|
||||
})
|
||||
|
||||
t.Run("PutAcceptsCaseInsensitiveKey", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
client := newMCPClient(t)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
cfg := createHonchoConfig(t, client)
|
||||
|
||||
// Request lowercases the canonical "X-User-Token".
|
||||
putResp, err := client.UpdateMCPServerUserHeaderValues(ctx, cfg.ID, codersdk.UpdateMCPServerUserHeaderValuesRequest{
|
||||
Values: map[string]string{"x-user-token": "jwt-a"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, putResp.HasValues["X-User-Token"], "HasValues should report under the canonical key")
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user