Thanks to visit codestin.com
Credit goes to github.com

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
8 changes: 8 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

77 changes: 48 additions & 29 deletions coderd/exp_chats.go
Original file line number Diff line number Diff line change
Expand Up @@ -1102,6 +1102,42 @@ func validateChatMCPServerIDs(
return unique, invalid, nil
}

// normalizeRequestedChatMCPServerIDs validates a request's MCP server
// selection for an existing chat. When requested is nil there is no
// change to make. IDs already persisted on the chat are exempt from the
// enabled-in-organization check: a server that is disabled or revoked
// after selection must not block sends. The generation path skips
// servers the chat can no longer use, and keeping the ID preserves the
// selection if the server is re-enabled. A non-nil response indicates
// the caller must write it with the returned status and stop.
func (api *API) normalizeRequestedChatMCPServerIDs(ctx context.Context, chat database.Chat, requested *[]uuid.UUID) (*[]uuid.UUID, int, *codersdk.Response) {
if requested == nil {
return nil, 0, nil
}
normalized, invalid, err := validateChatMCPServerIDs(ctx, api.Database, chat.OrganizationID, *requested)
if err != nil {
return nil, http.StatusInternalServerError, &codersdk.Response{
Message: "Failed to validate MCP server IDs.",
Detail: err.Error(),
}
}
persisted := make(map[uuid.UUID]struct{}, len(chat.MCPServerIDs))
for _, id := range chat.MCPServerIDs {
persisted[id] = struct{}{}
}
newlyInvalid := make([]uuid.UUID, 0, len(invalid))
for _, id := range invalid {
if _, ok := persisted[id]; !ok {
newlyInvalid = append(newlyInvalid, id)
}
}
if len(newlyInvalid) > 0 {
resp := invalidChatMCPServerIDsResponse(newlyInvalid)
return nil, http.StatusBadRequest, &resp
}
return &normalized, 0, nil
}

func invalidChatMCPServerIDsResponse(ids []uuid.UUID) codersdk.Response {
invalid := make([]string, 0, len(ids))
for _, id := range ids {
Expand Down Expand Up @@ -2555,36 +2591,12 @@ func (api *API) postChatMessages(rw http.ResponseWriter, r *http.Request) {
return
}

if req.MCPServerIDs != nil {
normalizedMCPServerIDs, invalidMCPServerIDs, err := validateChatMCPServerIDs(ctx, api.Database, chat.OrganizationID, *req.MCPServerIDs)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to validate MCP server IDs.",
Detail: err.Error(),
})
return
}
req.MCPServerIDs = &normalizedMCPServerIDs
// IDs already persisted on the chat are exempt: a server that
// is disabled or revoked after selection must not block sends.
// The generation path skips servers the chat can no longer use,
// and keeping the ID preserves the selection if the server is
// re-enabled.
persisted := make(map[uuid.UUID]struct{}, len(chat.MCPServerIDs))
for _, id := range chat.MCPServerIDs {
persisted[id] = struct{}{}
}
newlyInvalid := make([]uuid.UUID, 0, len(invalidMCPServerIDs))
for _, id := range invalidMCPServerIDs {
if _, ok := persisted[id]; !ok {
newlyInvalid = append(newlyInvalid, id)
}
}
if len(newlyInvalid) > 0 {
httpapi.Write(ctx, rw, http.StatusBadRequest, invalidChatMCPServerIDsResponse(newlyInvalid))
return
}
normalizedMCPServerIDs, status, mcpResp := api.normalizeRequestedChatMCPServerIDs(ctx, chat, req.MCPServerIDs)
if mcpResp != nil {
httpapi.Write(ctx, rw, status, *mcpResp)
return
}
req.MCPServerIDs = normalizedMCPServerIDs

if req.PlanMode != nil {
if !validateChatPlanMode(*req.PlanMode) {
Expand Down Expand Up @@ -2803,13 +2815,20 @@ func (api *API) patchChatMessage(rw http.ResponseWriter, r *http.Request) {
return
}

editMCPServerIDs, status, mcpResp := api.normalizeRequestedChatMCPServerIDs(ctx, chat, req.MCPServerIDs)
if mcpResp != nil {
httpapi.Write(ctx, rw, status, *mcpResp)
return
}

editResult, editErr := api.chatDaemon.EditMessage(ctx, chatd.EditMessageOptions{
ChatID: chat.ID,
CreatedBy: apiKey.UserID,
EditedMessageID: messageID,
Content: contentBlocks,
ModelConfigID: editModelConfigID,
ReasoningEffort: editReasoningEffort,
MCPServerIDs: editMCPServerIDs,
})
if editErr != nil {
if writeChatHookErr(ctx, rw, editErr, "Chat message denied by lifecycle hook.") {
Expand Down
92 changes: 92 additions & 0 deletions coderd/exp_chats_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9942,6 +9942,98 @@ func TestPatchChatMessage(t *testing.T) {
require.False(t, foundOriginalInChat)
})

t.Run("MCPServerIDsApplied", func(t *testing.T) {
t.Parallel()

ctx := testutil.Context(t, testutil.WaitLong)
client, db := newChatClientWithDatabase(t)
firstUser := coderdtest.CreateFirstUser(t, client.Client)
_ = createChatModel(t, client)

orgConfig := dbgen.MCPServerConfig(t, db, database.MCPServerConfig{
OrganizationID: firstUser.OrganizationID,
Enabled: true,
})

chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
OrganizationID: firstUser.OrganizationID,
Content: []codersdk.ChatInputPart{{
Type: codersdk.ChatInputPartTypeText,
Text: "before mcp edit",
}},
})
require.NoError(t, err)
require.Empty(t, chat.MCPServerIDs)

messagesResult, err := client.GetChatMessages(ctx, chat.ID, nil)
require.NoError(t, err)
userMessageID := messagesResult.Messages[0].ID

edited, err := client.EditChatMessage(ctx, chat.ID, userMessageID, codersdk.EditChatMessageRequest{
Content: []codersdk.ChatInputPart{{
Type: codersdk.ChatInputPartTypeText,
Text: "edit enabling the mcp server",
}},
MCPServerIDs: &[]uuid.UUID{orgConfig.ID},
})
require.NoError(t, err)

storedChat, err := db.GetChatByID(dbauthz.AsSystemRestricted(ctx), chat.ID)
require.NoError(t, err)
require.Equal(t, []uuid.UUID{orgConfig.ID}, storedChat.MCPServerIDs)

// Omitting the field preserves the persisted selection.
_, err = client.EditChatMessage(ctx, chat.ID, edited.Message.ID, codersdk.EditChatMessageRequest{
Content: []codersdk.ChatInputPart{{
Type: codersdk.ChatInputPartTypeText,
Text: "edit without an mcp selection",
}},
})
require.NoError(t, err)

storedChat, err = db.GetChatByID(dbauthz.AsSystemRestricted(ctx), chat.ID)
require.NoError(t, err)
require.Equal(t, []uuid.UUID{orgConfig.ID}, storedChat.MCPServerIDs)
})

t.Run("MCPServerIDsInvalidRejected", func(t *testing.T) {
t.Parallel()

ctx := testutil.Context(t, testutil.WaitLong)
client, db := newChatClientWithDatabase(t)
firstUser := coderdtest.CreateFirstUser(t, client.Client)
_ = createChatModel(t, client)

chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
OrganizationID: firstUser.OrganizationID,
Content: []codersdk.ChatInputPart{{
Type: codersdk.ChatInputPartTypeText,
Text: "before invalid mcp edit",
}},
})
require.NoError(t, err)

messagesResult, err := client.GetChatMessages(ctx, chat.ID, nil)
require.NoError(t, err)
userMessageID := messagesResult.Messages[0].ID

unknownID := uuid.New()
_, err = client.EditChatMessage(ctx, chat.ID, userMessageID, codersdk.EditChatMessageRequest{
Content: []codersdk.ChatInputPart{{
Type: codersdk.ChatInputPartTypeText,
Text: "edit selecting an unknown mcp server",
}},
MCPServerIDs: &[]uuid.UUID{unknownID},
})
sdkErr := requireSDKError(t, err, http.StatusBadRequest)
require.Equal(t, "One or more MCP server IDs are invalid or disabled.", sdkErr.Message)
require.Equal(t, "Invalid IDs: "+unknownID.String(), sdkErr.Detail)

storedChat, err := db.GetChatByID(dbauthz.AsSystemRestricted(ctx), chat.ID)
require.NoError(t, err)
require.Empty(t, storedChat.MCPServerIDs)
})

t.Run("CrossOrgModelConfigRejected", func(t *testing.T) {
t.Parallel()

Expand Down
66 changes: 42 additions & 24 deletions coderd/x/chatd/chatd.go
Original file line number Diff line number Diff line change
Expand Up @@ -1173,6 +1173,10 @@ type EditMessageOptions struct {
// original message's model is preserved.
ModelConfigID uuid.UUID
ReasoningEffort *string
// MCPServerIDs, when non-nil, replaces the chat's MCP server
// selection before the replacement turn runs. When nil the
// current selection is preserved.
MCPServerIDs *[]uuid.UUID
}

// EditMessageResult contains the replacement user message and chat status.
Expand Down Expand Up @@ -1243,6 +1247,36 @@ func enforceForcedMCPServerIDs(ctx context.Context, store database.Store, organi
return merged, nil
}

// applyRequestedMCPServerIDs replaces the chat's MCP server selection
// inside the state-machine transaction when a request provides one.
// Explore child chats keep the spawn-time snapshot immutable. Force On
// MCP servers are enforced server-side so a caller cannot remove them
// by tampering with the update (Cure53 CDM-02-010).
func (p *Server) applyRequestedMCPServerIDs(ctx context.Context, store database.Store, lockedChat database.Chat, requested *[]uuid.UUID) (database.Chat, error) {
if requested == nil {
return lockedChat, nil
}
if isExploreSubagentMode(lockedChat.Mode) {
p.logger.Warn(ctx,
"ignoring explore subagent mcp server ids update, snapshot is immutable after spawn",
slog.F("chat_id", lockedChat.ID),
)
return lockedChat, nil
}
enforcedIDs, err := enforceForcedMCPServerIDs(ctx, store, lockedChat.OrganizationID, lockedChat.OwnerID, *requested)
if err != nil {
return database.Chat{}, err
}
updated, err := store.UpdateChatMCPServerIDs(ctx, database.UpdateChatMCPServerIDsParams{
ID: lockedChat.ID,
MCPServerIDs: enforcedIDs,
})
if err != nil {
return database.Chat{}, xerrors.Errorf("update chat mcp server ids: %w", err)
}
return updated, nil
}

// CreateChat creates a chat with its initial history through
// chatstate.CreateChat. The new chat starts in `running` status per
// the chat execution state model. Ownership hints wake chat workers.
Expand Down Expand Up @@ -1514,30 +1548,9 @@ func (p *Server) SendMessage(
return err
}

// Update MCP server IDs on the chat when explicitly provided.
// Explore child chats keep the spawn-time snapshot immutable.
if requestedMCPServerIDs != nil {
if isExploreSubagentMode(lockedChat.Mode) {
p.logger.Warn(ctx,
"ignoring explore subagent mcp server ids update, snapshot is immutable after spawn",
slog.F("chat_id", opts.ChatID),
)
} else {
// Force On MCP servers are enforced server-side so a
// caller cannot remove them by tampering with the
// update (Cure53 CDM-02-010).
enforcedIDs, enforceErr := enforceForcedMCPServerIDs(ctx, store, lockedChat.OrganizationID, lockedChat.OwnerID, *requestedMCPServerIDs)
if enforceErr != nil {
return enforceErr
}
lockedChat, err = store.UpdateChatMCPServerIDs(ctx, database.UpdateChatMCPServerIDsParams{
ID: opts.ChatID,
MCPServerIDs: enforcedIDs,
})
if err != nil {
return xerrors.Errorf("update chat mcp server ids: %w", err)
}
}
lockedChat, err = p.applyRequestedMCPServerIDs(ctx, store, lockedChat, requestedMCPServerIDs)
if err != nil {
return err
}

messageCreatedBy := opts.CreatedBy
Expand Down Expand Up @@ -1885,6 +1898,11 @@ func (p *Server) EditMessage(
}
editedMsg = target

lockedChat, err = p.applyRequestedMCPServerIDs(ctx, store, lockedChat, opts.MCPServerIDs)
Comment thread
ibetitsmike marked this conversation as resolved.
Comment thread
ibetitsmike marked this conversation as resolved.
if err != nil {
return err
}

modelOverride, err := validateModelConfigOverride(ctx, store, lockedChat.OrganizationID, opts.ModelConfigID)
if err != nil {
return err
Expand Down
Loading
Loading