mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add opt-in Coder identity headers for MCP servers (#25153)
This commit is contained in:
@@ -229,6 +229,7 @@ func MCPServerConfig(t testing.TB, db database.Store, seed database.MCPServerCon
|
||||
Enabled: takeFirst(seed.Enabled, true),
|
||||
ModelIntent: seed.ModelIntent,
|
||||
AllowInPlanMode: seed.AllowInPlanMode,
|
||||
ForwardCoderHeaders: seed.ForwardCoderHeaders,
|
||||
CreatedBy: createdBy,
|
||||
UpdatedBy: updatedBy,
|
||||
})
|
||||
|
||||
Generated
+1
@@ -1800,6 +1800,7 @@ CREATE TABLE mcp_server_configs (
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
model_intent boolean DEFAULT false NOT NULL,
|
||||
allow_in_plan_mode boolean DEFAULT false NOT NULL,
|
||||
forward_coder_headers boolean DEFAULT false NOT NULL,
|
||||
CONSTRAINT mcp_server_configs_auth_type_check CHECK ((auth_type = ANY (ARRAY['none'::text, 'oauth2'::text, 'api_key'::text, 'custom_headers'::text, 'user_oidc'::text]))),
|
||||
CONSTRAINT mcp_server_configs_availability_check CHECK ((availability = ANY (ARRAY['force_on'::text, 'default_on'::text, 'default_off'::text]))),
|
||||
CONSTRAINT mcp_server_configs_transport_check CHECK ((transport = ANY (ARRAY['streamable_http'::text, 'sse'::text])))
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE mcp_server_configs
|
||||
DROP COLUMN forward_coder_headers;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE mcp_server_configs
|
||||
ADD COLUMN forward_coder_headers BOOLEAN NOT NULL DEFAULT false;
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
-- Migration 491 adds forward_coder_headers with a default of false.
|
||||
-- Flip the existing fixture row to true here so fixture data exercises
|
||||
-- the non-default state only after the column exists.
|
||||
UPDATE mcp_server_configs
|
||||
SET forward_coder_headers = TRUE
|
||||
WHERE id = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
|
||||
@@ -4765,6 +4765,7 @@ type MCPServerConfig struct {
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
ModelIntent bool `db:"model_intent" json:"model_intent"`
|
||||
AllowInPlanMode bool `db:"allow_in_plan_mode" json:"allow_in_plan_mode"`
|
||||
ForwardCoderHeaders bool `db:"forward_coder_headers" json:"forward_coder_headers"`
|
||||
}
|
||||
|
||||
type MCPServerUserToken struct {
|
||||
|
||||
@@ -13209,7 +13209,7 @@ func (q *sqlQuerier) DeleteMCPServerUserToken(ctx context.Context, arg DeleteMCP
|
||||
|
||||
const getEnabledMCPServerConfigs = `-- name: GetEnabledMCPServerConfigs :many
|
||||
SELECT
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers
|
||||
FROM
|
||||
mcp_server_configs
|
||||
WHERE
|
||||
@@ -13257,6 +13257,7 @@ func (q *sqlQuerier) GetEnabledMCPServerConfigs(ctx context.Context) ([]MCPServe
|
||||
&i.UpdatedAt,
|
||||
&i.ModelIntent,
|
||||
&i.AllowInPlanMode,
|
||||
&i.ForwardCoderHeaders,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -13273,7 +13274,7 @@ func (q *sqlQuerier) GetEnabledMCPServerConfigs(ctx context.Context) ([]MCPServe
|
||||
|
||||
const getForcedMCPServerConfigs = `-- name: GetForcedMCPServerConfigs :many
|
||||
SELECT
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers
|
||||
FROM
|
||||
mcp_server_configs
|
||||
WHERE
|
||||
@@ -13322,6 +13323,7 @@ func (q *sqlQuerier) GetForcedMCPServerConfigs(ctx context.Context) ([]MCPServer
|
||||
&i.UpdatedAt,
|
||||
&i.ModelIntent,
|
||||
&i.AllowInPlanMode,
|
||||
&i.ForwardCoderHeaders,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -13338,7 +13340,7 @@ func (q *sqlQuerier) GetForcedMCPServerConfigs(ctx context.Context) ([]MCPServer
|
||||
|
||||
const getMCPServerConfigByID = `-- name: GetMCPServerConfigByID :one
|
||||
SELECT
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers
|
||||
FROM
|
||||
mcp_server_configs
|
||||
WHERE
|
||||
@@ -13378,13 +13380,14 @@ func (q *sqlQuerier) GetMCPServerConfigByID(ctx context.Context, id uuid.UUID) (
|
||||
&i.UpdatedAt,
|
||||
&i.ModelIntent,
|
||||
&i.AllowInPlanMode,
|
||||
&i.ForwardCoderHeaders,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getMCPServerConfigBySlug = `-- name: GetMCPServerConfigBySlug :one
|
||||
SELECT
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers
|
||||
FROM
|
||||
mcp_server_configs
|
||||
WHERE
|
||||
@@ -13424,13 +13427,14 @@ func (q *sqlQuerier) GetMCPServerConfigBySlug(ctx context.Context, slug string)
|
||||
&i.UpdatedAt,
|
||||
&i.ModelIntent,
|
||||
&i.AllowInPlanMode,
|
||||
&i.ForwardCoderHeaders,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getMCPServerConfigs = `-- name: GetMCPServerConfigs :many
|
||||
SELECT
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers
|
||||
FROM
|
||||
mcp_server_configs
|
||||
ORDER BY
|
||||
@@ -13476,6 +13480,7 @@ func (q *sqlQuerier) GetMCPServerConfigs(ctx context.Context) ([]MCPServerConfig
|
||||
&i.UpdatedAt,
|
||||
&i.ModelIntent,
|
||||
&i.AllowInPlanMode,
|
||||
&i.ForwardCoderHeaders,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -13492,7 +13497,7 @@ func (q *sqlQuerier) GetMCPServerConfigs(ctx context.Context) ([]MCPServerConfig
|
||||
|
||||
const getMCPServerConfigsByIDs = `-- name: GetMCPServerConfigsByIDs :many
|
||||
SELECT
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers
|
||||
FROM
|
||||
mcp_server_configs
|
||||
WHERE
|
||||
@@ -13540,6 +13545,7 @@ func (q *sqlQuerier) GetMCPServerConfigsByIDs(ctx context.Context, ids []uuid.UU
|
||||
&i.UpdatedAt,
|
||||
&i.ModelIntent,
|
||||
&i.AllowInPlanMode,
|
||||
&i.ForwardCoderHeaders,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -13658,6 +13664,7 @@ INSERT INTO mcp_server_configs (
|
||||
enabled,
|
||||
model_intent,
|
||||
allow_in_plan_mode,
|
||||
forward_coder_headers,
|
||||
created_by,
|
||||
updated_by
|
||||
) VALUES (
|
||||
@@ -13685,11 +13692,12 @@ INSERT INTO mcp_server_configs (
|
||||
$22::boolean,
|
||||
$23::boolean,
|
||||
$24::boolean,
|
||||
$25::uuid,
|
||||
$26::uuid
|
||||
$25::boolean,
|
||||
$26::uuid,
|
||||
$27::uuid
|
||||
)
|
||||
RETURNING
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers
|
||||
`
|
||||
|
||||
type InsertMCPServerConfigParams struct {
|
||||
@@ -13717,6 +13725,7 @@ type InsertMCPServerConfigParams struct {
|
||||
Enabled bool `db:"enabled" json:"enabled"`
|
||||
ModelIntent bool `db:"model_intent" json:"model_intent"`
|
||||
AllowInPlanMode bool `db:"allow_in_plan_mode" json:"allow_in_plan_mode"`
|
||||
ForwardCoderHeaders bool `db:"forward_coder_headers" json:"forward_coder_headers"`
|
||||
CreatedBy uuid.UUID `db:"created_by" json:"created_by"`
|
||||
UpdatedBy uuid.UUID `db:"updated_by" json:"updated_by"`
|
||||
}
|
||||
@@ -13747,6 +13756,7 @@ func (q *sqlQuerier) InsertMCPServerConfig(ctx context.Context, arg InsertMCPSer
|
||||
arg.Enabled,
|
||||
arg.ModelIntent,
|
||||
arg.AllowInPlanMode,
|
||||
arg.ForwardCoderHeaders,
|
||||
arg.CreatedBy,
|
||||
arg.UpdatedBy,
|
||||
)
|
||||
@@ -13781,6 +13791,7 @@ func (q *sqlQuerier) InsertMCPServerConfig(ctx context.Context, arg InsertMCPSer
|
||||
&i.UpdatedAt,
|
||||
&i.ModelIntent,
|
||||
&i.AllowInPlanMode,
|
||||
&i.ForwardCoderHeaders,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -13813,12 +13824,13 @@ SET
|
||||
enabled = $22::boolean,
|
||||
model_intent = $23::boolean,
|
||||
allow_in_plan_mode = $24::boolean,
|
||||
updated_by = $25::uuid,
|
||||
forward_coder_headers = $25::boolean,
|
||||
updated_by = $26::uuid,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = $26::uuid
|
||||
id = $27::uuid
|
||||
RETURNING
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode
|
||||
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers
|
||||
`
|
||||
|
||||
type UpdateMCPServerConfigParams struct {
|
||||
@@ -13846,6 +13858,7 @@ type UpdateMCPServerConfigParams struct {
|
||||
Enabled bool `db:"enabled" json:"enabled"`
|
||||
ModelIntent bool `db:"model_intent" json:"model_intent"`
|
||||
AllowInPlanMode bool `db:"allow_in_plan_mode" json:"allow_in_plan_mode"`
|
||||
ForwardCoderHeaders bool `db:"forward_coder_headers" json:"forward_coder_headers"`
|
||||
UpdatedBy uuid.UUID `db:"updated_by" json:"updated_by"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
}
|
||||
@@ -13876,6 +13889,7 @@ func (q *sqlQuerier) UpdateMCPServerConfig(ctx context.Context, arg UpdateMCPSer
|
||||
arg.Enabled,
|
||||
arg.ModelIntent,
|
||||
arg.AllowInPlanMode,
|
||||
arg.ForwardCoderHeaders,
|
||||
arg.UpdatedBy,
|
||||
arg.ID,
|
||||
)
|
||||
@@ -13910,6 +13924,7 @@ func (q *sqlQuerier) UpdateMCPServerConfig(ctx context.Context, arg UpdateMCPSer
|
||||
&i.UpdatedAt,
|
||||
&i.ModelIntent,
|
||||
&i.AllowInPlanMode,
|
||||
&i.ForwardCoderHeaders,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ INSERT INTO mcp_server_configs (
|
||||
enabled,
|
||||
model_intent,
|
||||
allow_in_plan_mode,
|
||||
forward_coder_headers,
|
||||
created_by,
|
||||
updated_by
|
||||
) VALUES (
|
||||
@@ -106,6 +107,7 @@ INSERT INTO mcp_server_configs (
|
||||
@enabled::boolean,
|
||||
@model_intent::boolean,
|
||||
@allow_in_plan_mode::boolean,
|
||||
@forward_coder_headers::boolean,
|
||||
@created_by::uuid,
|
||||
@updated_by::uuid
|
||||
)
|
||||
@@ -140,6 +142,7 @@ SET
|
||||
enabled = @enabled::boolean,
|
||||
model_intent = @model_intent::boolean,
|
||||
allow_in_plan_mode = @allow_in_plan_mode::boolean,
|
||||
forward_coder_headers = @forward_coder_headers::boolean,
|
||||
updated_by = @updated_by::uuid,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
|
||||
+15
-5
@@ -283,6 +283,7 @@ func (api *API) createMCPServerConfig(rw http.ResponseWriter, r *http.Request) {
|
||||
Enabled: req.Enabled,
|
||||
ModelIntent: req.ModelIntent,
|
||||
AllowInPlanMode: req.AllowInPlanMode,
|
||||
ForwardCoderHeaders: req.ForwardCoderHeaders,
|
||||
CreatedBy: apiKey.UserID,
|
||||
UpdatedBy: apiKey.UserID,
|
||||
})
|
||||
@@ -371,6 +372,7 @@ func (api *API) createMCPServerConfig(rw http.ResponseWriter, r *http.Request) {
|
||||
Enabled: inserted.Enabled,
|
||||
ModelIntent: inserted.ModelIntent,
|
||||
AllowInPlanMode: inserted.AllowInPlanMode,
|
||||
ForwardCoderHeaders: inserted.ForwardCoderHeaders,
|
||||
UpdatedBy: apiKey.UserID,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -440,6 +442,7 @@ func (api *API) createMCPServerConfig(rw http.ResponseWriter, r *http.Request) {
|
||||
Enabled: req.Enabled,
|
||||
ModelIntent: req.ModelIntent,
|
||||
AllowInPlanMode: req.AllowInPlanMode,
|
||||
ForwardCoderHeaders: req.ForwardCoderHeaders,
|
||||
CreatedBy: apiKey.UserID,
|
||||
UpdatedBy: apiKey.UserID,
|
||||
})
|
||||
@@ -699,6 +702,11 @@ func (api *API) updateMCPServerConfig(rw http.ResponseWriter, r *http.Request) {
|
||||
allowInPlanMode = *req.AllowInPlanMode
|
||||
}
|
||||
|
||||
forwardCoderHeaders := existing.ForwardCoderHeaders
|
||||
if req.ForwardCoderHeaders != nil {
|
||||
forwardCoderHeaders = *req.ForwardCoderHeaders
|
||||
}
|
||||
|
||||
// When auth_type changes, clear fields belonging to the
|
||||
// previous auth type so stale secrets don't persist.
|
||||
if authType != existing.AuthType {
|
||||
@@ -783,6 +791,7 @@ func (api *API) updateMCPServerConfig(rw http.ResponseWriter, r *http.Request) {
|
||||
Enabled: enabled,
|
||||
ModelIntent: modelIntent,
|
||||
AllowInPlanMode: allowInPlanMode,
|
||||
ForwardCoderHeaders: forwardCoderHeaders,
|
||||
UpdatedBy: apiKey.UserID,
|
||||
ID: existing.ID,
|
||||
})
|
||||
@@ -1264,11 +1273,12 @@ func convertMCPServerConfig(config database.MCPServerConfig) codersdk.MCPServerC
|
||||
|
||||
Availability: config.Availability,
|
||||
|
||||
Enabled: config.Enabled,
|
||||
ModelIntent: config.ModelIntent,
|
||||
AllowInPlanMode: config.AllowInPlanMode,
|
||||
CreatedAt: config.CreatedAt,
|
||||
UpdatedAt: config.UpdatedAt,
|
||||
Enabled: config.Enabled,
|
||||
ModelIntent: config.ModelIntent,
|
||||
AllowInPlanMode: config.AllowInPlanMode,
|
||||
ForwardCoderHeaders: config.ForwardCoderHeaders,
|
||||
CreatedAt: config.CreatedAt,
|
||||
UpdatedAt: config.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-4
@@ -99,6 +99,7 @@ func TestMCPServerConfigsCRUD(t *testing.T) {
|
||||
require.Equal(t, "default_on", created.Availability)
|
||||
require.True(t, created.Enabled)
|
||||
require.False(t, created.AllowInPlanMode)
|
||||
require.False(t, created.ForwardCoderHeaders)
|
||||
|
||||
// Verify the secret is indicated but never returned.
|
||||
require.True(t, created.HasOAuth2Secret)
|
||||
@@ -110,25 +111,31 @@ func TestMCPServerConfigsCRUD(t *testing.T) {
|
||||
require.Equal(t, created.ID, configs[0].ID)
|
||||
require.True(t, configs[0].HasOAuth2Secret)
|
||||
require.False(t, configs[0].AllowInPlanMode)
|
||||
require.False(t, configs[0].ForwardCoderHeaders)
|
||||
|
||||
fetched, err := client.MCPServerConfigByID(ctx, created.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, created.ID, fetched.ID)
|
||||
require.False(t, fetched.AllowInPlanMode)
|
||||
require.False(t, fetched.ForwardCoderHeaders)
|
||||
|
||||
// Update display name, availability, and allow_in_plan_mode.
|
||||
// Update display name, availability, allow_in_plan_mode, and
|
||||
// forward_coder_headers.
|
||||
newName := "Renamed Server"
|
||||
newAvail := "force_on"
|
||||
allowInPlanMode := true
|
||||
forwardCoderHeaders := true
|
||||
updated, err := client.UpdateMCPServerConfig(ctx, created.ID, codersdk.UpdateMCPServerConfigRequest{
|
||||
DisplayName: &newName,
|
||||
Availability: &newAvail,
|
||||
AllowInPlanMode: &allowInPlanMode,
|
||||
DisplayName: &newName,
|
||||
Availability: &newAvail,
|
||||
AllowInPlanMode: &allowInPlanMode,
|
||||
ForwardCoderHeaders: &forwardCoderHeaders,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Renamed Server", updated.DisplayName)
|
||||
require.Equal(t, "force_on", updated.Availability)
|
||||
require.True(t, updated.AllowInPlanMode)
|
||||
require.True(t, updated.ForwardCoderHeaders)
|
||||
// Unchanged fields should remain the same.
|
||||
require.Equal(t, "my-mcp-server", updated.Slug)
|
||||
require.Equal(t, "oauth2", updated.AuthType)
|
||||
@@ -140,10 +147,12 @@ func TestMCPServerConfigsCRUD(t *testing.T) {
|
||||
require.Equal(t, "Renamed Server", configs[0].DisplayName)
|
||||
require.Equal(t, "force_on", configs[0].Availability)
|
||||
require.True(t, configs[0].AllowInPlanMode)
|
||||
require.True(t, configs[0].ForwardCoderHeaders)
|
||||
|
||||
fetched, err = client.MCPServerConfigByID(ctx, created.ID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, fetched.AllowInPlanMode)
|
||||
require.True(t, fetched.ForwardCoderHeaders)
|
||||
|
||||
// Delete it.
|
||||
err = client.DeleteMCPServerConfig(ctx, created.ID)
|
||||
|
||||
@@ -6977,6 +6977,7 @@ func (p *Server) runChat(
|
||||
mcpTokens = p.refreshExpiredMCPTokens(ctx, logger, mcpConnectConfigs, mcpTokens)
|
||||
mcpTools, mcpCleanup = mcpclient.ConnectAll(
|
||||
ctx, logger, mcpConnectConfigs, mcpTokens, chat.OwnerID, p.oidcTokenSource,
|
||||
chatprovider.CoderHeaders(chat),
|
||||
)
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
package mcpclient_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"charm.land/fantasy"
|
||||
"github.com/google/uuid"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
mcpserver "github.com/mark3labs/mcp-go/server"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"cdr.dev/slog/v3/sloggers/slogtest"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/x/chatd/chatprovider"
|
||||
"github.com/coder/coder/v2/coderd/x/chatd/mcpclient"
|
||||
)
|
||||
|
||||
// newHeaderRecordingServer creates a streamable HTTP MCP server with a
|
||||
// single "ping" tool. Every request's headers are appended to the
|
||||
// returned slice so tests can assert which headers were forwarded.
|
||||
func newHeaderRecordingServer(t *testing.T) (*httptest.Server, *sync.Mutex, *[]http.Header) {
|
||||
t.Helper()
|
||||
var (
|
||||
mu sync.Mutex
|
||||
headers []http.Header
|
||||
)
|
||||
srv := mcpserver.NewMCPServer("hdr-server", "1.0.0")
|
||||
srv.AddTools(mcpserver.ServerTool{
|
||||
Tool: mcp.NewTool("ping", mcp.WithDescription("records the request headers")),
|
||||
Handler: func(_ context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
mu.Lock()
|
||||
headers = append(headers, req.Header.Clone())
|
||||
mu.Unlock()
|
||||
return mcp.NewToolResultText("ok"), nil
|
||||
},
|
||||
})
|
||||
httpSrv := mcpserver.NewStreamableHTTPServer(srv)
|
||||
ts := httptest.NewServer(httpSrv)
|
||||
t.Cleanup(ts.Close)
|
||||
return ts, &mu, &headers
|
||||
}
|
||||
|
||||
// TestConnectAll_ForwardCoderHeaders_DefaultOff is a regression guard
|
||||
// that the Coder identity headers are NOT sent when the option is
|
||||
// left at its default (false).
|
||||
func TestConnectAll_ForwardCoderHeaders_DefaultOff(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
|
||||
|
||||
ts, mu, recorded := newHeaderRecordingServer(t)
|
||||
|
||||
cfg := makeConfig("no-hdr", ts.URL)
|
||||
assert.False(t, cfg.ForwardCoderHeaders, "default must be false")
|
||||
|
||||
coderHeaders := map[string]string{
|
||||
chatprovider.HeaderCoderOwnerID: uuid.NewString(),
|
||||
chatprovider.HeaderCoderChatID: uuid.NewString(),
|
||||
chatprovider.HeaderCoderWorkspaceID: uuid.NewString(),
|
||||
}
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(
|
||||
ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil,
|
||||
coderHeaders,
|
||||
)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
_, err := tools[0].Run(ctx, fantasy.ToolCall{
|
||||
ID: "call-1", Name: "no-hdr__ping", Input: "{}",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
require.NotEmpty(t, *recorded)
|
||||
for _, h := range *recorded {
|
||||
assert.Empty(t, h.Get(chatprovider.HeaderCoderOwnerID))
|
||||
assert.Empty(t, h.Get(chatprovider.HeaderCoderChatID))
|
||||
assert.Empty(t, h.Get(chatprovider.HeaderCoderSubchatID))
|
||||
assert.Empty(t, h.Get(chatprovider.HeaderCoderWorkspaceID))
|
||||
}
|
||||
}
|
||||
|
||||
// TestConnectAll_ForwardCoderHeaders_Enabled verifies that when the
|
||||
// option is enabled, the Coder identity headers are forwarded on every
|
||||
// outgoing MCP request, including the subchat and workspace headers.
|
||||
func TestConnectAll_ForwardCoderHeaders_Enabled(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
|
||||
|
||||
ts, mu, recorded := newHeaderRecordingServer(t)
|
||||
|
||||
ownerID := uuid.New()
|
||||
chatID := uuid.New()
|
||||
workspaceID := uuid.New()
|
||||
subchatID := uuid.New()
|
||||
|
||||
cfg := makeConfig("hdr", ts.URL)
|
||||
cfg.ForwardCoderHeaders = true
|
||||
|
||||
// Subchat headers: parent's chat ID lives in X-Coder-Chat-Id, the
|
||||
// subchat's own ID lives in X-Coder-Subchat-Id.
|
||||
coderHeaders := chatprovider.CoderHeaders(database.Chat{
|
||||
ID: subchatID,
|
||||
OwnerID: ownerID,
|
||||
ParentChatID: uuid.NullUUID{UUID: chatID, Valid: true},
|
||||
WorkspaceID: uuid.NullUUID{UUID: workspaceID, Valid: true},
|
||||
})
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(
|
||||
ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil,
|
||||
coderHeaders,
|
||||
)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
_, err := tools[0].Run(ctx, fantasy.ToolCall{
|
||||
ID: "call-1", Name: "hdr__ping", Input: "{}",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
require.NotEmpty(t, *recorded)
|
||||
last := (*recorded)[len(*recorded)-1]
|
||||
assert.Equal(t, ownerID.String(), last.Get(chatprovider.HeaderCoderOwnerID))
|
||||
assert.Equal(t, chatID.String(), last.Get(chatprovider.HeaderCoderChatID))
|
||||
assert.Equal(t, subchatID.String(), last.Get(chatprovider.HeaderCoderSubchatID))
|
||||
assert.Equal(t, workspaceID.String(), last.Get(chatprovider.HeaderCoderWorkspaceID))
|
||||
}
|
||||
|
||||
// TestConnectAll_ForwardCoderHeaders_RootChat verifies that for a root
|
||||
// chat (no parent), the chat's own ID is forwarded as
|
||||
// X-Coder-Chat-Id and the X-Coder-Subchat-Id header is absent.
|
||||
func TestConnectAll_ForwardCoderHeaders_RootChat(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
|
||||
|
||||
ts, mu, recorded := newHeaderRecordingServer(t)
|
||||
|
||||
ownerID := uuid.New()
|
||||
chatID := uuid.New()
|
||||
|
||||
cfg := makeConfig("hdr-root", ts.URL)
|
||||
cfg.ForwardCoderHeaders = true
|
||||
|
||||
coderHeaders := chatprovider.CoderHeaders(database.Chat{
|
||||
ID: chatID,
|
||||
OwnerID: ownerID,
|
||||
})
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(
|
||||
ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil,
|
||||
coderHeaders,
|
||||
)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
_, err := tools[0].Run(ctx, fantasy.ToolCall{
|
||||
ID: "call-1", Name: "hdr-root__ping", Input: "{}",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
require.NotEmpty(t, *recorded)
|
||||
last := (*recorded)[len(*recorded)-1]
|
||||
assert.Equal(t, ownerID.String(), last.Get(chatprovider.HeaderCoderOwnerID))
|
||||
assert.Equal(t, chatID.String(), last.Get(chatprovider.HeaderCoderChatID))
|
||||
assert.Empty(t, last.Get(chatprovider.HeaderCoderSubchatID))
|
||||
assert.Empty(t, last.Get(chatprovider.HeaderCoderWorkspaceID))
|
||||
}
|
||||
|
||||
// TestConnectAll_ForwardCoderHeaders_WithAPIKeyAuth verifies that the
|
||||
// api_key auth header is preserved when Coder identity headers are
|
||||
// forwarded alongside.
|
||||
func TestConnectAll_ForwardCoderHeaders_WithAPIKeyAuth(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
|
||||
|
||||
ts, mu, recorded := newHeaderRecordingServer(t)
|
||||
|
||||
ownerID := uuid.New()
|
||||
chatID := uuid.New()
|
||||
|
||||
cfg := makeConfig("hdr-apikey", ts.URL)
|
||||
cfg.AuthType = "api_key"
|
||||
cfg.APIKeyHeader = "X-Api-Key"
|
||||
cfg.APIKeyValue = "sekret"
|
||||
cfg.ForwardCoderHeaders = true
|
||||
|
||||
coderHeaders := chatprovider.CoderHeaders(database.Chat{
|
||||
ID: chatID,
|
||||
OwnerID: ownerID,
|
||||
})
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(
|
||||
ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil,
|
||||
coderHeaders,
|
||||
)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
_, err := tools[0].Run(ctx, fantasy.ToolCall{
|
||||
ID: "call-1", Name: "hdr-apikey__ping", Input: "{}",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
require.NotEmpty(t, *recorded)
|
||||
last := (*recorded)[len(*recorded)-1]
|
||||
assert.Equal(t, "sekret", last.Get("X-Api-Key"))
|
||||
assert.Equal(t, ownerID.String(), last.Get(chatprovider.HeaderCoderOwnerID))
|
||||
assert.Equal(t, chatID.String(), last.Get(chatprovider.HeaderCoderChatID))
|
||||
}
|
||||
|
||||
// TestConnectAll_ForwardCoderHeaders_WithOAuth2 verifies that the
|
||||
// oauth2 Authorization header is preserved when Coder identity
|
||||
// headers are forwarded alongside, and that auth wins on a conflict.
|
||||
func TestConnectAll_ForwardCoderHeaders_WithOAuth2(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
|
||||
|
||||
ts, mu, recorded := newHeaderRecordingServer(t)
|
||||
|
||||
cfgID := uuid.New()
|
||||
cfg := makeConfig("hdr-oauth", ts.URL)
|
||||
cfg.ID = cfgID
|
||||
cfg.AuthType = "oauth2"
|
||||
cfg.ForwardCoderHeaders = true
|
||||
token := database.MCPServerUserToken{
|
||||
MCPServerConfigID: cfgID,
|
||||
AccessToken: "oauth-token-xyz",
|
||||
TokenType: "Bearer",
|
||||
}
|
||||
|
||||
// Intentionally include an Authorization key to verify the auth
|
||||
// header wins on conflict.
|
||||
ownerID := uuid.NewString()
|
||||
coderHeaders := map[string]string{
|
||||
"Authorization": "Bearer should-be-overridden",
|
||||
chatprovider.HeaderCoderOwnerID: ownerID,
|
||||
}
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(
|
||||
ctx, logger,
|
||||
[]database.MCPServerConfig{cfg},
|
||||
[]database.MCPServerUserToken{token},
|
||||
uuid.Nil, nil,
|
||||
coderHeaders,
|
||||
)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
_, err := tools[0].Run(ctx, fantasy.ToolCall{
|
||||
ID: "call-1", Name: "hdr-oauth__ping", Input: "{}",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
require.NotEmpty(t, *recorded)
|
||||
last := (*recorded)[len(*recorded)-1]
|
||||
assert.Equal(t, "Bearer oauth-token-xyz", last.Get("Authorization"))
|
||||
assert.Equal(t, ownerID, last.Get(chatprovider.HeaderCoderOwnerID))
|
||||
}
|
||||
|
||||
// TestConnectAll_ForwardCoderHeaders_WithCustomHeaders verifies that
|
||||
// custom_headers admin-configured values are preserved when Coder
|
||||
// identity headers are forwarded alongside, including the case where
|
||||
// the admin configures a custom header whose name only differs from a
|
||||
// Coder identity header by case. Conflict detection is case-
|
||||
// insensitive because http.Header.Set canonicalizes header names.
|
||||
func TestConnectAll_ForwardCoderHeaders_WithCustomHeaders(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := t.Context()
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
|
||||
|
||||
ts, mu, recorded := newHeaderRecordingServer(t)
|
||||
|
||||
ownerID := uuid.New()
|
||||
chatID := uuid.New()
|
||||
|
||||
cfg := makeConfig("hdr-custom", ts.URL)
|
||||
cfg.AuthType = "custom_headers"
|
||||
// Include both an unrelated custom header AND a case-variant of
|
||||
// X-Coder-Owner-Id to exercise the case-insensitive conflict
|
||||
// check. The admin-configured value MUST win.
|
||||
cfg.CustomHeaders = `{"X-Tenant":"acme","x-coder-owner-id":"admin-controlled"}`
|
||||
cfg.ForwardCoderHeaders = true
|
||||
|
||||
coderHeaders := chatprovider.CoderHeaders(database.Chat{
|
||||
ID: chatID,
|
||||
OwnerID: ownerID,
|
||||
})
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(
|
||||
ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil,
|
||||
coderHeaders,
|
||||
)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
_, err := tools[0].Run(ctx, fantasy.ToolCall{
|
||||
ID: "call-1", Name: "hdr-custom__ping", Input: "{}",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
require.NotEmpty(t, *recorded)
|
||||
last := (*recorded)[len(*recorded)-1]
|
||||
assert.Equal(t, "acme", last.Get("X-Tenant"))
|
||||
// The admin's case-variant header must win, because HTTP header
|
||||
// names are case-insensitive at the transport level.
|
||||
assert.Equal(t, "admin-controlled", last.Get(chatprovider.HeaderCoderOwnerID))
|
||||
assert.Equal(t, chatID.String(), last.Get(chatprovider.HeaderCoderChatID))
|
||||
}
|
||||
@@ -74,6 +74,7 @@ func ConnectAll(
|
||||
tokens []database.MCPServerUserToken,
|
||||
userID uuid.UUID,
|
||||
oidcSrc UserOIDCTokenSource,
|
||||
coderHeaders map[string]string,
|
||||
) ([]fantasy.AgentTool, func()) {
|
||||
// Index tokens by server config ID so auth header
|
||||
// construction is O(1) per server.
|
||||
@@ -109,7 +110,7 @@ func ConnectAll(
|
||||
|
||||
eg.Go(func() error {
|
||||
serverTools, mcpClient, connectErr := connectOne(
|
||||
ctx, logger, cfg, tokensByConfigID, userID, oidcSrc,
|
||||
ctx, logger, cfg, tokensByConfigID, userID, oidcSrc, coderHeaders,
|
||||
)
|
||||
if connectErr != nil {
|
||||
logger.Warn(ctx,
|
||||
@@ -175,9 +176,31 @@ func connectOne(
|
||||
tokensByConfigID map[uuid.UUID]database.MCPServerUserToken,
|
||||
userID uuid.UUID,
|
||||
oidcSrc UserOIDCTokenSource,
|
||||
coderHeaders map[string]string,
|
||||
) ([]fantasy.AgentTool, *client.Client, error) {
|
||||
headers := buildAuthHeaders(ctx, logger, cfg, tokensByConfigID, userID, oidcSrc)
|
||||
|
||||
// When opted-in, merge Coder identity headers BEFORE the
|
||||
// transport is created so any auth header already set above
|
||||
// wins on a conflict. Conflict detection uses
|
||||
// http.CanonicalHeaderKey because the upstream transport applies
|
||||
// http.Header.Set, which canonicalizes keys; without that, an
|
||||
// admin-configured header that differs only in case from a Coder
|
||||
// identity header would land in the request map twice and the
|
||||
// surviving value would be non-deterministic.
|
||||
if cfg.ForwardCoderHeaders {
|
||||
canonicalAuth := make(map[string]struct{}, len(headers))
|
||||
for k := range headers {
|
||||
canonicalAuth[http.CanonicalHeaderKey(k)] = struct{}{}
|
||||
}
|
||||
for k, v := range coderHeaders {
|
||||
if _, exists := canonicalAuth[http.CanonicalHeaderKey(k)]; exists {
|
||||
continue
|
||||
}
|
||||
headers[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
tr, err := createTransport(cfg, headers)
|
||||
if err != nil {
|
||||
return nil, nil, xerrors.Errorf(
|
||||
|
||||
@@ -96,7 +96,7 @@ func TestConnectAll_DiscoverTools(t *testing.T) {
|
||||
ts := newTestMCPServer(t, echoTool(), greetTool())
|
||||
|
||||
cfg := makeConfig("myserver", ts.URL)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil, nil)
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
// Two tools should be discovered, namespaced with the server slug.
|
||||
@@ -121,7 +121,7 @@ func TestConnectAll_CallTool(t *testing.T) {
|
||||
ts := newTestMCPServer(t, echoTool())
|
||||
|
||||
cfg := makeConfig("srv", ts.URL)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil, nil)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
@@ -147,7 +147,7 @@ func TestConnectAll_ToolAllowList(t *testing.T) {
|
||||
// Only allow the "echo" tool.
|
||||
cfg.ToolAllowList = []string{"echo"}
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil, nil)
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
require.Len(t, tools, 1)
|
||||
@@ -165,7 +165,7 @@ func TestConnectAll_ToolDenyList(t *testing.T) {
|
||||
// Deny the "greet" tool, so only "echo" remains.
|
||||
cfg.ToolDenyList = []string{"greet"}
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil, nil)
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
require.Len(t, tools, 1)
|
||||
@@ -179,7 +179,7 @@ func TestConnectAll_ConnectionFailure(t *testing.T) {
|
||||
|
||||
cfg := makeConfig("bad", "http://127.0.0.1:0/does-not-exist")
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil, nil)
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
assert.Empty(t, tools, "no tools should be returned for an unreachable server")
|
||||
@@ -201,6 +201,7 @@ func TestConnectAll_MultipleServers(t *testing.T) {
|
||||
[]database.MCPServerConfig{cfg1, cfg2},
|
||||
nil,
|
||||
uuid.Nil, nil,
|
||||
nil,
|
||||
)
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
@@ -227,6 +228,7 @@ func TestConnectAll_NoToolsAfterFiltering(t *testing.T) {
|
||||
[]database.MCPServerConfig{cfg},
|
||||
nil,
|
||||
uuid.Nil, nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
require.Empty(t, tools)
|
||||
@@ -255,6 +257,7 @@ func TestConnectAll_DeterministicOrder(t *testing.T) {
|
||||
},
|
||||
nil,
|
||||
uuid.Nil, nil,
|
||||
nil,
|
||||
)
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
@@ -284,6 +287,7 @@ func TestConnectAll_DeterministicOrder(t *testing.T) {
|
||||
},
|
||||
nil,
|
||||
uuid.Nil, nil,
|
||||
nil,
|
||||
)
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
@@ -317,6 +321,7 @@ func TestConnectAll_DeterministicOrder(t *testing.T) {
|
||||
[]database.MCPServerConfig{cfg1, cfg2},
|
||||
nil,
|
||||
uuid.Nil, nil,
|
||||
nil,
|
||||
)
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
@@ -381,6 +386,7 @@ func TestConnectAll_AuthHeaders(t *testing.T) {
|
||||
[]database.MCPServerConfig{cfg},
|
||||
[]database.MCPServerUserToken{token},
|
||||
uuid.Nil, nil,
|
||||
nil,
|
||||
)
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
@@ -435,7 +441,7 @@ func TestConnectAll_DisabledServer(t *testing.T) {
|
||||
cfg := makeConfig("disabled", ts.URL)
|
||||
cfg.Enabled = false
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil, nil)
|
||||
t.Cleanup(cleanup)
|
||||
assert.Empty(t, tools)
|
||||
}
|
||||
@@ -450,7 +456,7 @@ func TestConnectAll_CallToolInvalidInput(t *testing.T) {
|
||||
ts := newTestMCPServer(t, echoTool())
|
||||
|
||||
cfg := makeConfig("srv", ts.URL)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil, nil)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
@@ -475,7 +481,7 @@ func TestConnectAll_ToolInfoParameters(t *testing.T) {
|
||||
ts := newTestMCPServer(t, echoTool())
|
||||
|
||||
cfg := makeConfig("srv", ts.URL)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil, nil)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
@@ -517,7 +523,7 @@ func TestConnectAll_NilRequiredBecomesEmptySlice(t *testing.T) {
|
||||
|
||||
ts := newTestMCPServer(t, noRequiredTool)
|
||||
cfg := makeConfig("srv", ts.URL)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil, nil)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
@@ -570,6 +576,7 @@ func TestConnectAll_APIKeyAuth(t *testing.T) {
|
||||
tools, cleanup := mcpclient.ConnectAll(
|
||||
ctx, logger, []database.MCPServerConfig{cfg}, nil,
|
||||
uuid.Nil, nil,
|
||||
nil,
|
||||
)
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
@@ -627,6 +634,7 @@ func TestConnectAll_CustomHeadersAuth(t *testing.T) {
|
||||
tools, cleanup := mcpclient.ConnectAll(
|
||||
ctx, logger, []database.MCPServerConfig{cfg}, nil,
|
||||
uuid.Nil, nil,
|
||||
nil,
|
||||
)
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
@@ -664,6 +672,7 @@ func TestConnectAll_CustomHeadersInvalidJSON(t *testing.T) {
|
||||
tools, cleanup := mcpclient.ConnectAll(
|
||||
ctx, logger, []database.MCPServerConfig{cfg}, nil,
|
||||
uuid.Nil, nil,
|
||||
nil,
|
||||
)
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
@@ -722,7 +731,7 @@ func TestConnectAll_UserOIDCAuth(t *testing.T) {
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(
|
||||
ctx, logger, []database.MCPServerConfig{cfg}, nil,
|
||||
userID, src,
|
||||
userID, src, nil,
|
||||
)
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
@@ -781,7 +790,7 @@ func TestConnectAll_UserOIDCAuth_NoLink(t *testing.T) {
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(
|
||||
ctx, logger, []database.MCPServerConfig{cfg}, nil,
|
||||
uuid.New(), src,
|
||||
uuid.New(), src, nil,
|
||||
)
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
@@ -817,7 +826,7 @@ func TestConnectAll_UserOIDCAuth_NilSource(t *testing.T) {
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(
|
||||
ctx, logger, []database.MCPServerConfig{cfg}, nil,
|
||||
uuid.New(), nil,
|
||||
uuid.New(), nil, nil,
|
||||
)
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
@@ -846,6 +855,7 @@ func TestConnectAll_ParallelConnections(t *testing.T) {
|
||||
[]database.MCPServerConfig{cfg1, cfg2, cfg3},
|
||||
nil,
|
||||
uuid.Nil, nil,
|
||||
nil,
|
||||
)
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
@@ -906,7 +916,7 @@ func TestConnectAll_ExpiredToken(t *testing.T) {
|
||||
Expiry: sql.NullTime{Time: time.Now().Add(-1 * time.Hour), Valid: true},
|
||||
}
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, []database.MCPServerUserToken{token}, uuid.Nil, nil)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, []database.MCPServerUserToken{token}, uuid.Nil, nil, nil)
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
// The server accepts any auth, so the tool is still discovered
|
||||
@@ -939,7 +949,7 @@ func TestConnectAll_EmptyAccessToken(t *testing.T) {
|
||||
TokenType: "Bearer",
|
||||
}
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, []database.MCPServerUserToken{token}, uuid.Nil, nil)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, []database.MCPServerUserToken{token}, uuid.Nil, nil, nil)
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
// Tool is still discovered (server doesn't require auth), but
|
||||
@@ -969,7 +979,7 @@ func TestConnectAll_MCPToolIdentifier(t *testing.T) {
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil, nil)
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
require.Len(t, tools, 1)
|
||||
@@ -1016,6 +1026,7 @@ func TestConnectAll_MCPToolIdentifier_MultipleServers(t *testing.T) {
|
||||
[]database.MCPServerConfig{cfg1, cfg2},
|
||||
nil,
|
||||
uuid.Nil, nil,
|
||||
nil,
|
||||
)
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
@@ -1072,7 +1083,7 @@ func TestConnectAll_EmbeddedResourceText(t *testing.T) {
|
||||
t.Cleanup(ts.Close)
|
||||
|
||||
cfg := makeConfig("embed-txt", ts.URL)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil, nil)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
@@ -1139,7 +1150,7 @@ func TestConnectAll_EmbeddedResourceBlob(t *testing.T) {
|
||||
t.Cleanup(ts.Close)
|
||||
|
||||
cfg := makeConfig("embed-blob", ts.URL)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil, nil)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
@@ -1219,7 +1230,7 @@ func TestConnectAll_ResourceLink(t *testing.T) {
|
||||
t.Cleanup(ts.Close)
|
||||
|
||||
cfg := makeConfig("res-link", ts.URL)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil, nil)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
@@ -1263,7 +1274,7 @@ func TestConnectAll_CallToolError(t *testing.T) {
|
||||
t.Cleanup(ts.Close)
|
||||
|
||||
cfg := makeConfig("err-srv", ts.URL)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil, nil)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
@@ -1287,7 +1298,7 @@ func TestModelIntent_Info_WrapsSchema(t *testing.T) {
|
||||
cfg := makeConfig("intent-srv", ts.URL)
|
||||
cfg.ModelIntent = true
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil, nil)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
@@ -1323,7 +1334,7 @@ func TestModelIntent_Info_NoWrapWhenDisabled(t *testing.T) {
|
||||
cfg := makeConfig("no-intent", ts.URL)
|
||||
cfg.ModelIntent = false
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil, nil)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
@@ -1346,7 +1357,7 @@ func TestModelIntent_Run_UnwrapsProperties(t *testing.T) {
|
||||
cfg := makeConfig("unwrap-srv", ts.URL)
|
||||
cfg.ModelIntent = true
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil, nil)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
@@ -1371,7 +1382,7 @@ func TestModelIntent_Run_UnwrapsFlat(t *testing.T) {
|
||||
cfg := makeConfig("flat-srv", ts.URL)
|
||||
cfg.ModelIntent = true
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil, nil)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
@@ -1396,7 +1407,7 @@ func TestModelIntent_Run_PassthroughWhenDisabled(t *testing.T) {
|
||||
cfg := makeConfig("pass-srv", ts.URL)
|
||||
cfg.ModelIntent = false
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil, nil)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
@@ -1421,7 +1432,7 @@ func TestModelIntent_Run_FallbackOnBadJSON(t *testing.T) {
|
||||
cfg := makeConfig("bad-srv", ts.URL)
|
||||
cfg.ModelIntent = true
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil)
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil, nil)
|
||||
t.Cleanup(cleanup)
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user