mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add MCP server config ID to tool-call message parts (#23522)
This commit is contained in:
+27
-1
@@ -3017,6 +3017,16 @@ func (p *Server) runChat(
|
||||
defer mcpCleanup()
|
||||
}
|
||||
|
||||
// Build a lookup from tool name to MCP server config ID
|
||||
// so we can annotate persisted parts with the originating
|
||||
// server.
|
||||
toolNameToConfigID := make(map[string]uuid.UUID)
|
||||
for _, t := range mcpTools {
|
||||
if mcp, ok := t.(mcpclient.MCPToolIdentifier); ok {
|
||||
toolNameToConfigID[t.Info().Name] = mcp.MCPServerConfigID()
|
||||
}
|
||||
}
|
||||
|
||||
if instruction != "" {
|
||||
prompt = chatprompt.InsertSystem(prompt, instruction)
|
||||
}
|
||||
@@ -3079,7 +3089,13 @@ func (p *Server) runChat(
|
||||
if len(assistantBlocks) > 0 {
|
||||
sdkParts := make([]codersdk.ChatMessagePart, 0, len(assistantBlocks))
|
||||
for _, block := range assistantBlocks {
|
||||
sdkParts = append(sdkParts, chatprompt.PartFromContent(block))
|
||||
part := chatprompt.PartFromContent(block)
|
||||
if part.ToolName != "" {
|
||||
if configID, ok := toolNameToConfigID[part.ToolName]; ok {
|
||||
part.MCPServerConfigID = uuid.NullUUID{UUID: configID, Valid: true}
|
||||
}
|
||||
}
|
||||
sdkParts = append(sdkParts, part)
|
||||
}
|
||||
finalAssistantText = strings.TrimSpace(contentBlocksToText(sdkParts))
|
||||
var marshalErr error
|
||||
@@ -3092,6 +3108,11 @@ func (p *Server) runChat(
|
||||
toolResultContents := make([]pqtype.NullRawMessage, len(toolResults))
|
||||
for i, tr := range toolResults {
|
||||
trPart := chatprompt.PartFromContent(tr)
|
||||
if trPart.ToolName != "" {
|
||||
if configID, ok := toolNameToConfigID[trPart.ToolName]; ok {
|
||||
trPart.MCPServerConfigID = uuid.NullUUID{UUID: configID, Valid: true}
|
||||
}
|
||||
}
|
||||
var marshalErr error
|
||||
toolResultContents[i], marshalErr = chatprompt.MarshalParts([]codersdk.ChatMessagePart{trPart})
|
||||
if marshalErr != nil {
|
||||
@@ -3463,6 +3484,11 @@ func (p *Server) runChat(
|
||||
role codersdk.ChatMessageRole,
|
||||
part codersdk.ChatMessagePart,
|
||||
) {
|
||||
if part.ToolName != "" {
|
||||
if configID, ok := toolNameToConfigID[part.ToolName]; ok {
|
||||
part.MCPServerConfigID = uuid.NullUUID{UUID: configID, Valid: true}
|
||||
}
|
||||
}
|
||||
p.publishMessagePart(chat.ID, role, part)
|
||||
},
|
||||
Compaction: compactionOptions,
|
||||
|
||||
@@ -195,7 +195,7 @@ func connectOne(
|
||||
}
|
||||
|
||||
tools = append(
|
||||
tools, newMCPTool(cfg.Slug, mcpTool, mcpClient),
|
||||
tools, newMCPTool(cfg.ID, cfg.Slug, mcpTool, mcpClient),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -383,10 +383,17 @@ func redactErrorURL(err error) string {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
// MCPToolIdentifier is implemented by tools that originate from
|
||||
// an MCP server config and can report the config's database ID.
|
||||
type MCPToolIdentifier interface {
|
||||
MCPServerConfigID() uuid.UUID
|
||||
}
|
||||
|
||||
// mcpToolWrapper adapts a single MCP tool into a
|
||||
// fantasy.AgentTool. It stores the prefixed name for Info() but
|
||||
// strips the prefix when forwarding calls to the remote server.
|
||||
type mcpToolWrapper struct {
|
||||
configID uuid.UUID
|
||||
prefixedName string
|
||||
originalName string
|
||||
description string
|
||||
@@ -396,14 +403,22 @@ type mcpToolWrapper struct {
|
||||
providerOptions fantasy.ProviderOptions
|
||||
}
|
||||
|
||||
// MCPServerConfigID returns the database ID of the MCP server
|
||||
// config that this tool originates from.
|
||||
func (t *mcpToolWrapper) MCPServerConfigID() uuid.UUID {
|
||||
return t.configID
|
||||
}
|
||||
|
||||
// newMCPTool creates an mcpToolWrapper from an mcp.Tool
|
||||
// discovered on a remote server.
|
||||
func newMCPTool(
|
||||
configID uuid.UUID,
|
||||
serverSlug string,
|
||||
tool mcp.Tool,
|
||||
mcpClient *client.Client,
|
||||
) *mcpToolWrapper {
|
||||
return &mcpToolWrapper{
|
||||
configID: configID,
|
||||
prefixedName: serverSlug + toolNameSep + tool.Name,
|
||||
originalName: tool.Name,
|
||||
description: tool.Description,
|
||||
|
||||
@@ -621,6 +621,91 @@ func TestConnectAll_EmptyAccessToken(t *testing.T) {
|
||||
require.NotEmpty(t, tools)
|
||||
}
|
||||
|
||||
// TestConnectAll_MCPToolIdentifier verifies that tools returned
|
||||
// by ConnectAll implement the MCPToolIdentifier interface and
|
||||
// report the correct server config ID.
|
||||
func TestConnectAll_MCPToolIdentifier(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
|
||||
|
||||
ts := newTestMCPServer(t, echoTool())
|
||||
|
||||
configID := uuid.New()
|
||||
cfg := database.MCPServerConfig{
|
||||
ID: configID,
|
||||
Slug: "id-srv",
|
||||
DisplayName: "ID Server",
|
||||
Url: ts.URL,
|
||||
Transport: "streamable_http",
|
||||
AuthType: "none",
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil)
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
require.Len(t, tools, 1)
|
||||
|
||||
// Assert the tool implements MCPToolIdentifier.
|
||||
identifier, ok := tools[0].(mcpclient.MCPToolIdentifier)
|
||||
require.True(t, ok, "tool should implement MCPToolIdentifier")
|
||||
assert.Equal(t, configID, identifier.MCPServerConfigID())
|
||||
}
|
||||
|
||||
// TestConnectAll_MCPToolIdentifier_MultipleServers verifies that
|
||||
// each tool from a different MCP server carries its own config ID.
|
||||
func TestConnectAll_MCPToolIdentifier_MultipleServers(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
|
||||
|
||||
ts1 := newTestMCPServer(t, echoTool())
|
||||
ts2 := newTestMCPServer(t, greetTool())
|
||||
|
||||
configID1 := uuid.New()
|
||||
configID2 := uuid.New()
|
||||
cfg1 := database.MCPServerConfig{
|
||||
ID: configID1,
|
||||
Slug: "srv-a",
|
||||
DisplayName: "Server A",
|
||||
Url: ts1.URL,
|
||||
Transport: "streamable_http",
|
||||
AuthType: "none",
|
||||
Enabled: true,
|
||||
}
|
||||
cfg2 := database.MCPServerConfig{
|
||||
ID: configID2,
|
||||
Slug: "srv-b",
|
||||
DisplayName: "Server B",
|
||||
Url: ts2.URL,
|
||||
Transport: "streamable_http",
|
||||
AuthType: "none",
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
tools, cleanup := mcpclient.ConnectAll(
|
||||
ctx, logger,
|
||||
[]database.MCPServerConfig{cfg1, cfg2},
|
||||
nil,
|
||||
)
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
require.Len(t, tools, 2)
|
||||
|
||||
// Map tool name to config ID via the MCPToolIdentifier
|
||||
// interface.
|
||||
idByName := make(map[string]uuid.UUID)
|
||||
for _, tool := range tools {
|
||||
identifier, ok := tool.(mcpclient.MCPToolIdentifier)
|
||||
require.True(t, ok, "tool %q should implement MCPToolIdentifier", tool.Info().Name)
|
||||
idByName[tool.Info().Name] = identifier.MCPServerConfigID()
|
||||
}
|
||||
|
||||
assert.Equal(t, configID1, idByName["srv-a__echo"])
|
||||
assert.Equal(t, configID2, idByName["srv-b__greet"])
|
||||
}
|
||||
|
||||
func TestConnectAll_CallToolError(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
|
||||
+20
-19
@@ -146,25 +146,26 @@ func AllChatMessagePartTypes() []ChatMessagePartType {
|
||||
// the frontend does not expect adds noise to the wire format
|
||||
// and wastes space in persisted chat_messages rows.
|
||||
type ChatMessagePart struct {
|
||||
Type ChatMessagePartType `json:"type"`
|
||||
Text string `json:"text" variants:"text,reasoning"`
|
||||
Signature string `json:"signature,omitempty"`
|
||||
ToolCallID string `json:"tool_call_id,omitempty" variants:"tool-call?,tool-result?"`
|
||||
ToolName string `json:"tool_name,omitempty" variants:"tool-call?,tool-result?"`
|
||||
Args json.RawMessage `json:"args,omitempty" variants:"tool-call?"`
|
||||
ArgsDelta string `json:"args_delta,omitempty" variants:"tool-call?"`
|
||||
Result json.RawMessage `json:"result,omitempty" variants:"tool-result?"`
|
||||
ResultDelta string `json:"result_delta,omitempty"`
|
||||
IsError bool `json:"is_error,omitempty" variants:"tool-result?"`
|
||||
SourceID string `json:"source_id,omitempty" variants:"source?"`
|
||||
URL string `json:"url" variants:"source"`
|
||||
Title string `json:"title,omitempty" variants:"source?"`
|
||||
MediaType string `json:"media_type" variants:"file"`
|
||||
Data []byte `json:"data,omitempty" variants:"file?"`
|
||||
FileID uuid.NullUUID `json:"file_id,omitempty" format:"uuid" variants:"file?"`
|
||||
FileName string `json:"file_name" variants:"file-reference"`
|
||||
StartLine int `json:"start_line" variants:"file-reference"`
|
||||
EndLine int `json:"end_line" variants:"file-reference"`
|
||||
Type ChatMessagePartType `json:"type"`
|
||||
Text string `json:"text" variants:"text,reasoning"`
|
||||
Signature string `json:"signature,omitempty"`
|
||||
ToolCallID string `json:"tool_call_id,omitempty" variants:"tool-call?,tool-result?"`
|
||||
ToolName string `json:"tool_name,omitempty" variants:"tool-call?,tool-result?"`
|
||||
MCPServerConfigID uuid.NullUUID `json:"mcp_server_config_id,omitempty" format:"uuid" variants:"tool-call?,tool-result?"`
|
||||
Args json.RawMessage `json:"args,omitempty" variants:"tool-call?"`
|
||||
ArgsDelta string `json:"args_delta,omitempty" variants:"tool-call?"`
|
||||
Result json.RawMessage `json:"result,omitempty" variants:"tool-result?"`
|
||||
ResultDelta string `json:"result_delta,omitempty"`
|
||||
IsError bool `json:"is_error,omitempty" variants:"tool-result?"`
|
||||
SourceID string `json:"source_id,omitempty" variants:"source?"`
|
||||
URL string `json:"url" variants:"source"`
|
||||
Title string `json:"title,omitempty" variants:"source?"`
|
||||
MediaType string `json:"media_type" variants:"file"`
|
||||
Data []byte `json:"data,omitempty" variants:"file?"`
|
||||
FileID uuid.NullUUID `json:"file_id,omitempty" format:"uuid" variants:"file?"`
|
||||
FileName string `json:"file_name" variants:"file-reference"`
|
||||
StartLine int `json:"start_line" variants:"file-reference"`
|
||||
EndLine int `json:"end_line" variants:"file-reference"`
|
||||
// The code content from the diff that was commented on.
|
||||
Content string `json:"content" variants:"file-reference"`
|
||||
// ProviderMetadata holds provider-specific response metadata
|
||||
|
||||
Generated
+2
@@ -1860,6 +1860,7 @@ export interface ChatToolCallPart {
|
||||
readonly type: "tool-call";
|
||||
readonly tool_call_id?: string;
|
||||
readonly tool_name?: string;
|
||||
readonly mcp_server_config_id?: string;
|
||||
readonly args?: Record<string, string>;
|
||||
readonly args_delta?: string;
|
||||
/**
|
||||
@@ -1874,6 +1875,7 @@ export interface ChatToolResultPart {
|
||||
readonly type: "tool-result";
|
||||
readonly tool_call_id?: string;
|
||||
readonly tool_name?: string;
|
||||
readonly mcp_server_config_id?: string;
|
||||
readonly result?: Record<string, string>;
|
||||
readonly is_error?: boolean;
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { FileDiff, File as FileViewer } from "@pierre/diffs/react";
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
import { ScrollArea } from "components/ScrollArea/ScrollArea";
|
||||
import { type ComponentPropsWithRef, type FC, memo } from "react";
|
||||
import { cn } from "utils/cn";
|
||||
@@ -52,6 +53,10 @@ interface ToolProps extends Omit<ComponentPropsWithRef<"div">, "children"> {
|
||||
subagentTitles?: Map<string, string>;
|
||||
/** Maps sub-agent chat IDs to real-time status updates from stream events. */
|
||||
subagentStatusOverrides?: Map<string, string>;
|
||||
/** MCP server config ID associated with this tool call. */
|
||||
mcpServerConfigId?: string;
|
||||
/** Available MCP server configs for icon/name lookup. */
|
||||
mcpServers?: readonly TypesGen.MCPServerConfig[];
|
||||
}
|
||||
|
||||
// Props passed to each tool-specific renderer function. Each renderer
|
||||
@@ -64,6 +69,8 @@ type ToolRendererProps = {
|
||||
isError: boolean;
|
||||
subagentTitles?: Map<string, string>;
|
||||
subagentStatusOverrides?: Map<string, string>;
|
||||
mcpServerConfigId?: string;
|
||||
mcpServers?: readonly TypesGen.MCPServerConfig[];
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -451,6 +458,8 @@ const GenericToolRenderer: FC<ToolRendererProps> = ({
|
||||
args,
|
||||
result,
|
||||
isError,
|
||||
mcpServerConfigId,
|
||||
mcpServers,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isDark = theme.palette.mode === "dark";
|
||||
@@ -466,11 +475,25 @@ const GenericToolRenderer: FC<ToolRendererProps> = ({
|
||||
}
|
||||
: fileViewerOpts;
|
||||
|
||||
// Look up MCP server config for icon and slug.
|
||||
const mcpServer = mcpServerConfigId
|
||||
? mcpServers?.find((s) => s.id === mcpServerConfigId)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<ToolIcon name={name} isError={status === "error" || isError} />
|
||||
<ToolLabel name={name} args={args} result={result} />
|
||||
<ToolIcon
|
||||
name={name}
|
||||
isError={status === "error" || isError}
|
||||
iconUrl={mcpServer?.icon_url}
|
||||
/>
|
||||
<ToolLabel
|
||||
name={name}
|
||||
args={args}
|
||||
result={result}
|
||||
mcpSlug={mcpServer?.slug}
|
||||
/>
|
||||
</div>
|
||||
{writeFileDiff ? (
|
||||
<ScrollArea
|
||||
@@ -557,6 +580,8 @@ export const Tool = memo(
|
||||
isError = false,
|
||||
subagentTitles,
|
||||
subagentStatusOverrides,
|
||||
mcpServerConfigId,
|
||||
mcpServers,
|
||||
ref,
|
||||
...props
|
||||
}: ToolProps) => {
|
||||
@@ -583,6 +608,8 @@ export const Tool = memo(
|
||||
isError={isError}
|
||||
subagentTitles={subagentTitles}
|
||||
subagentStatusOverrides={subagentStatusOverrides}
|
||||
mcpServerConfigId={mcpServerConfigId}
|
||||
mcpServers={mcpServers}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ExternalImage } from "components/ExternalImage/ExternalImage";
|
||||
import {
|
||||
BotIcon,
|
||||
ClipboardListIcon,
|
||||
@@ -9,14 +10,38 @@ import {
|
||||
WrenchIcon,
|
||||
} from "lucide-react";
|
||||
import type React from "react";
|
||||
import { useState } from "react";
|
||||
import { cn } from "utils/cn";
|
||||
|
||||
export const ToolIcon: React.FC<{ name: string; isError: boolean }> = ({
|
||||
name,
|
||||
isError,
|
||||
}) => {
|
||||
export const ToolIcon: React.FC<{
|
||||
name: string;
|
||||
isError: boolean;
|
||||
iconUrl?: string;
|
||||
}> = ({ name, isError, iconUrl }) => {
|
||||
const [imgError, setImgError] = useState(false);
|
||||
const color = isError ? "text-content-destructive" : "text-content-secondary";
|
||||
const base = cn("h-4 w-4 shrink-0", color);
|
||||
|
||||
// If an MCP icon URL is provided and hasn't failed, render it.
|
||||
if (iconUrl && !imgError) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-4 w-4 shrink-0 items-center justify-center",
|
||||
"rounded-full bg-surface-secondary",
|
||||
isError && "ring-1 ring-content-destructive",
|
||||
)}
|
||||
>
|
||||
<ExternalImage
|
||||
src={iconUrl}
|
||||
alt={`${name} icon`}
|
||||
className="h-3 w-3"
|
||||
onError={() => setImgError(true)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
switch (name) {
|
||||
case "execute":
|
||||
case "process_output":
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type React from "react";
|
||||
import { asRecord, asString, parseArgs } from "./utils";
|
||||
import { asRecord, asString, humanizeMCPToolName, parseArgs } from "./utils";
|
||||
|
||||
export const ToolLabel: React.FC<{
|
||||
name: string;
|
||||
args: unknown;
|
||||
result: unknown;
|
||||
}> = ({ name, args, result }) => {
|
||||
mcpSlug?: string;
|
||||
}> = ({ name, args, result, mcpSlug }) => {
|
||||
const parsed = parseArgs(args);
|
||||
const parsedResult = asRecord(result);
|
||||
|
||||
@@ -169,9 +170,13 @@ export const ToolLabel: React.FC<{
|
||||
</span>
|
||||
);
|
||||
}
|
||||
default:
|
||||
default: {
|
||||
const displayName = mcpSlug ? humanizeMCPToolName(mcpSlug, name) : name;
|
||||
return (
|
||||
<span className="truncate text-sm text-content-secondary">{name}</span>
|
||||
<span className="truncate text-sm text-content-secondary">
|
||||
{displayName}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
getFileViewerOptionsMinimal,
|
||||
getFileViewerOptionsNoHeader,
|
||||
getWriteFileDiff,
|
||||
humanizeMCPToolName,
|
||||
isSubagentRunningStatus,
|
||||
isSubagentSuccessStatus,
|
||||
mapSubagentStatusToToolStatus,
|
||||
@@ -667,6 +668,36 @@ describe("stripSvnIndexHeaders", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("humanizeMCPToolName", () => {
|
||||
it("strips slug prefix and humanizes", () => {
|
||||
expect(humanizeMCPToolName("linear", "linear__list_issues")).toBe(
|
||||
"List issues",
|
||||
);
|
||||
});
|
||||
|
||||
it("handles single-word tool name after prefix", () => {
|
||||
expect(humanizeMCPToolName("github", "github__search")).toBe("Search");
|
||||
});
|
||||
|
||||
it("humanizes entire name when prefix does not match", () => {
|
||||
expect(humanizeMCPToolName("linear", "github__list_repos")).toBe(
|
||||
"Github list repos",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to prefixedName when stripping prefix leaves empty string", () => {
|
||||
expect(humanizeMCPToolName("linear", "linear__")).toBe("linear__");
|
||||
});
|
||||
|
||||
it("collapses consecutive underscores into a single space", () => {
|
||||
expect(humanizeMCPToolName("srv", "srv__get__data")).toBe("Get data");
|
||||
});
|
||||
|
||||
it("humanizes tool name without slug prefix", () => {
|
||||
expect(humanizeMCPToolName("linear", "list_issues")).toBe("List issues");
|
||||
});
|
||||
});
|
||||
|
||||
describe("constants", () => {
|
||||
it("COLLAPSED_OUTPUT_HEIGHT is 54", () => {
|
||||
expect(COLLAPSED_OUTPUT_HEIGHT).toBe(54);
|
||||
|
||||
@@ -555,6 +555,26 @@ export const buildEditDiff = (
|
||||
return parsed[0].files[0];
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts an MCP-prefixed tool name into a human-readable label.
|
||||
* E.g. "linear__list_issues" with slug "linear" → "List issues"
|
||||
*/
|
||||
export function humanizeMCPToolName(
|
||||
slug: string,
|
||||
prefixedName: string,
|
||||
): string {
|
||||
const prefix = `${slug}__`;
|
||||
const raw = prefixedName.startsWith(prefix)
|
||||
? prefixedName.slice(prefix.length)
|
||||
: prefixedName;
|
||||
// Replace runs of underscores with a single space, then trim.
|
||||
const words = raw.replace(/_+/g, " ").trim();
|
||||
if (!words) {
|
||||
return prefixedName;
|
||||
}
|
||||
return words.charAt(0).toUpperCase() + words.slice(1);
|
||||
}
|
||||
|
||||
// Re-export runtime type utils used by sub-components so they
|
||||
// can import from a single location.
|
||||
export { asNumber, asRecord, asString } from "../runtimeTypeUtils";
|
||||
|
||||
@@ -97,6 +97,7 @@ type RenderBlockListParams = {
|
||||
isStreaming?: boolean;
|
||||
subagentTitles?: Map<string, string>;
|
||||
subagentStatusOverrides?: Map<string, TypesGen.ChatStatus>;
|
||||
mcpServers?: readonly TypesGen.MCPServerConfig[];
|
||||
onImageClick?: (src: string) => void;
|
||||
onTextFileClick?: (content: string) => void;
|
||||
urlTransform?: UrlTransform;
|
||||
@@ -265,6 +266,7 @@ function renderBlockList({
|
||||
isStreaming = false,
|
||||
subagentTitles,
|
||||
subagentStatusOverrides,
|
||||
mcpServers,
|
||||
onImageClick,
|
||||
onTextFileClick,
|
||||
urlTransform,
|
||||
@@ -329,6 +331,7 @@ function renderBlockList({
|
||||
isError={false}
|
||||
subagentTitles={subagentTitles}
|
||||
subagentStatusOverrides={subagentStatusOverrides}
|
||||
mcpServers={mcpServers}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -345,6 +348,8 @@ function renderBlockList({
|
||||
subagentStatusOverrides={
|
||||
isStreaming ? subagentStatusOverrides : undefined
|
||||
}
|
||||
mcpServerConfigId={tool.mcpServerConfigId}
|
||||
mcpServers={mcpServers}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -386,6 +391,7 @@ interface ChatMessageItemProps {
|
||||
// overlay to indicate truncated content.
|
||||
fadeFromBottom?: boolean;
|
||||
urlTransform?: UrlTransform;
|
||||
mcpServers?: readonly TypesGen.MCPServerConfig[];
|
||||
}
|
||||
|
||||
const ChatMessageItem: FC<ChatMessageItemProps> = ({
|
||||
@@ -397,6 +403,7 @@ const ChatMessageItem: FC<ChatMessageItemProps> = ({
|
||||
isAfterEditingMessage = false,
|
||||
fadeFromBottom = false,
|
||||
urlTransform,
|
||||
mcpServers,
|
||||
}) => {
|
||||
const isUser = message.role === "user";
|
||||
const isSavingMessage = savingMessageId === message.id;
|
||||
@@ -462,6 +469,7 @@ const ChatMessageItem: FC<ChatMessageItemProps> = ({
|
||||
onImageClick: setPreviewImage,
|
||||
onTextFileClick: (content) => setPreviewText(content),
|
||||
urlTransform,
|
||||
mcpServers,
|
||||
});
|
||||
const remainingTools = parsed.tools.filter(
|
||||
(tool) => !renderedToolIDs.has(tool.id),
|
||||
@@ -599,6 +607,8 @@ const ChatMessageItem: FC<ChatMessageItemProps> = ({
|
||||
result={tool.result}
|
||||
status={tool.status}
|
||||
isError={tool.isError}
|
||||
mcpServerConfigId={tool.mcpServerConfigId}
|
||||
mcpServers={mcpServers}
|
||||
/>
|
||||
))}
|
||||
{!hasRenderableContent && (
|
||||
@@ -635,6 +645,7 @@ export const StreamingOutput: FC<{
|
||||
showInitialPlaceholder?: boolean;
|
||||
retryState?: { attempt: number; error: string } | null;
|
||||
urlTransform?: UrlTransform;
|
||||
mcpServers?: readonly TypesGen.MCPServerConfig[];
|
||||
}> = ({
|
||||
streamState,
|
||||
streamTools,
|
||||
@@ -643,6 +654,7 @@ export const StreamingOutput: FC<{
|
||||
showInitialPlaceholder = false,
|
||||
retryState,
|
||||
urlTransform,
|
||||
mcpServers,
|
||||
}) => {
|
||||
const conversationItemProps = { role: "assistant" as const };
|
||||
const toolByID = new Map(streamTools.map((tool) => [tool.id, tool]));
|
||||
@@ -655,6 +667,7 @@ export const StreamingOutput: FC<{
|
||||
subagentTitles,
|
||||
subagentStatusOverrides,
|
||||
urlTransform,
|
||||
mcpServers,
|
||||
});
|
||||
const remainingTools = streamTools.filter(
|
||||
(tool) => !renderedToolIDs.has(tool.id),
|
||||
@@ -696,6 +709,8 @@ export const StreamingOutput: FC<{
|
||||
isError={tool.isError}
|
||||
subagentTitles={subagentTitles}
|
||||
subagentStatusOverrides={subagentStatusOverrides}
|
||||
mcpServerConfigId={tool.mcpServerConfigId}
|
||||
mcpServers={mcpServers}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -989,6 +1004,7 @@ interface ConversationTimelineProps {
|
||||
editingMessageId?: number | null;
|
||||
savingMessageId?: number | null;
|
||||
urlTransform?: UrlTransform;
|
||||
mcpServers?: readonly TypesGen.MCPServerConfig[];
|
||||
}
|
||||
|
||||
export const ConversationTimeline: FC<ConversationTimelineProps> = ({
|
||||
@@ -1006,6 +1022,7 @@ export const ConversationTimeline: FC<ConversationTimelineProps> = ({
|
||||
editingMessageId,
|
||||
savingMessageId,
|
||||
urlTransform,
|
||||
mcpServers,
|
||||
}) => {
|
||||
const shouldRenderStreamAfterMessages =
|
||||
hasStreamOutput && parsedMessages.length > 0;
|
||||
@@ -1054,6 +1071,7 @@ export const ConversationTimeline: FC<ConversationTimelineProps> = ({
|
||||
savingMessageId={savingMessageId}
|
||||
urlTransform={urlTransform}
|
||||
isAfterEditingMessage={afterEditingMessageIds.has(message.id)}
|
||||
mcpServers={mcpServers}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
@@ -1066,6 +1084,7 @@ export const ConversationTimeline: FC<ConversationTimelineProps> = ({
|
||||
showInitialPlaceholder={isAwaitingFirstStreamChunk}
|
||||
retryState={retryState}
|
||||
urlTransform={urlTransform}
|
||||
mcpServers={mcpServers}
|
||||
/>
|
||||
)}
|
||||
{hasStreamOutput && parsedMessages.length === 0 && (
|
||||
@@ -1077,6 +1096,7 @@ export const ConversationTimeline: FC<ConversationTimelineProps> = ({
|
||||
showInitialPlaceholder={isAwaitingFirstStreamChunk}
|
||||
retryState={retryState}
|
||||
urlTransform={urlTransform}
|
||||
mcpServers={mcpServers}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -97,6 +97,7 @@ export const mergeTools = (
|
||||
result: result?.result,
|
||||
isError: result?.isError ?? false,
|
||||
status: result ? (result.isError ? "error" : "completed") : "completed",
|
||||
mcpServerConfigId: call.mcpServerConfigId || result?.mcpServerConfigId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -108,6 +109,7 @@ export const mergeTools = (
|
||||
result: result.result,
|
||||
isError: result.isError,
|
||||
status: result.isError ? "error" : "completed",
|
||||
mcpServerConfigId: result.mcpServerConfigId,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -148,6 +150,7 @@ export const parseMessageContent = (
|
||||
id,
|
||||
name: part.tool_name || "Tool",
|
||||
args: part.args,
|
||||
mcpServerConfigId: part.mcp_server_config_id,
|
||||
});
|
||||
parsed.blocks = ensureToolBlock(parsed.blocks, id);
|
||||
break;
|
||||
@@ -168,6 +171,7 @@ export const parseMessageContent = (
|
||||
name,
|
||||
result: part.result,
|
||||
isError: parseToolResultIsError(name, part, part.result),
|
||||
mcpServerConfigId: part.mcp_server_config_id,
|
||||
});
|
||||
parsed.blocks = ensureToolBlock(parsed.blocks, id);
|
||||
break;
|
||||
|
||||
@@ -70,6 +70,8 @@ export const applyMessagePartToStreamState = (
|
||||
name: part.tool_name || existing?.name || "Tool",
|
||||
args: nextArgs.value,
|
||||
argsRaw: nextArgs.rawText,
|
||||
mcpServerConfigId:
|
||||
part.mcp_server_config_id || existing?.mcpServerConfigId,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -115,6 +117,8 @@ export const applyMessagePartToStreamState = (
|
||||
result: nextResult.value,
|
||||
resultRaw: nextResult.rawText,
|
||||
isError: nextIsError,
|
||||
mcpServerConfigId:
|
||||
part.mcp_server_config_id || existing?.mcpServerConfigId,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -192,6 +196,7 @@ export const buildStreamTools = (
|
||||
result: result?.result,
|
||||
isError: result?.isError ?? false,
|
||||
status: result ? (result.isError ? "error" : "completed") : "running",
|
||||
mcpServerConfigId: call.mcpServerConfigId || result?.mcpServerConfigId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -203,6 +208,7 @@ export const buildStreamTools = (
|
||||
result: result.result,
|
||||
isError: result.isError,
|
||||
status: result.isError ? "error" : "completed",
|
||||
mcpServerConfigId: result.mcpServerConfigId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ export type ParsedToolCall = {
|
||||
id: string;
|
||||
name: string;
|
||||
args?: unknown;
|
||||
mcpServerConfigId?: string;
|
||||
};
|
||||
|
||||
export type ParsedToolResult = {
|
||||
@@ -11,6 +12,7 @@ export type ParsedToolResult = {
|
||||
name: string;
|
||||
result?: unknown;
|
||||
isError: boolean;
|
||||
mcpServerConfigId?: string;
|
||||
};
|
||||
|
||||
export type MergedTool = {
|
||||
@@ -20,6 +22,7 @@ export type MergedTool = {
|
||||
result?: unknown;
|
||||
isError: boolean;
|
||||
status: "completed" | "error" | "running";
|
||||
mcpServerConfigId?: string;
|
||||
};
|
||||
|
||||
export type RenderBlock =
|
||||
@@ -62,6 +65,7 @@ type StreamToolCall = {
|
||||
name: string;
|
||||
args?: unknown;
|
||||
argsRaw?: string;
|
||||
mcpServerConfigId?: string;
|
||||
};
|
||||
|
||||
type StreamToolResult = {
|
||||
@@ -70,6 +74,7 @@ type StreamToolResult = {
|
||||
result?: unknown;
|
||||
resultRaw?: string;
|
||||
isError: boolean;
|
||||
mcpServerConfigId?: string;
|
||||
};
|
||||
|
||||
export type StreamState = {
|
||||
|
||||
@@ -51,6 +51,7 @@ interface AgentDetailTimelineProps {
|
||||
editingMessageId?: number | null;
|
||||
savingMessageId?: number | null;
|
||||
urlTransform?: UrlTransform;
|
||||
mcpServers?: readonly TypesGen.MCPServerConfig[];
|
||||
}
|
||||
|
||||
// Reads only message-related store state (stable during streaming).
|
||||
@@ -63,6 +64,7 @@ const MessageListProvider: FC<AgentDetailTimelineProps> = ({
|
||||
editingMessageId,
|
||||
savingMessageId,
|
||||
urlTransform,
|
||||
mcpServers,
|
||||
}) => {
|
||||
const messagesByID = useChatSelector(store, selectMessagesByID);
|
||||
const orderedMessageIDs = useChatSelector(store, selectOrderedMessageIDs);
|
||||
@@ -105,6 +107,7 @@ const MessageListProvider: FC<AgentDetailTimelineProps> = ({
|
||||
editingMessageId={editingMessageId}
|
||||
savingMessageId={savingMessageId}
|
||||
urlTransform={urlTransform}
|
||||
mcpServers={mcpServers}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -129,6 +132,7 @@ const StreamingBridge: FC<{
|
||||
editingMessageId?: number | null;
|
||||
savingMessageId?: number | null;
|
||||
urlTransform?: UrlTransform;
|
||||
mcpServers?: readonly TypesGen.MCPServerConfig[];
|
||||
}> = ({
|
||||
store,
|
||||
isEmpty,
|
||||
@@ -143,6 +147,7 @@ const StreamingBridge: FC<{
|
||||
editingMessageId,
|
||||
savingMessageId,
|
||||
urlTransform,
|
||||
mcpServers,
|
||||
}) => {
|
||||
const streamState = useChatSelector(store, selectStreamState);
|
||||
const streamTools = buildStreamTools(streamState);
|
||||
@@ -170,6 +175,7 @@ const StreamingBridge: FC<{
|
||||
editingMessageId={editingMessageId}
|
||||
savingMessageId={savingMessageId}
|
||||
urlTransform={urlTransform}
|
||||
mcpServers={mcpServers}
|
||||
/>
|
||||
</Profiler>
|
||||
);
|
||||
|
||||
@@ -282,6 +282,7 @@ export const AgentDetailView: FC<AgentDetailViewProps> = ({
|
||||
editingMessageId={editing.editingMessageId}
|
||||
savingMessageId={pendingEditMessageId}
|
||||
urlTransform={urlTransform}
|
||||
mcpServers={mcpServers}
|
||||
/>
|
||||
</div>
|
||||
</ScrollAnchoredContainer>
|
||||
|
||||
Reference in New Issue
Block a user