fix(api): use camelCase in CallToolResponse and add type discriminators to ContentBlock (#7487)
Co-authored-by: Goose <opensource@block.xyz> Co-authored-by: Jack Amadeo <jackamadeo@block.xyz>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -54,9 +54,9 @@ export type CallToolRequest = {
|
||||
|
||||
export type CallToolResponse = {
|
||||
_meta?: unknown;
|
||||
content: Array<Content>;
|
||||
is_error: boolean;
|
||||
structured_content?: unknown;
|
||||
content: Array<ContentBlock>;
|
||||
isError: boolean;
|
||||
structuredContent?: unknown;
|
||||
};
|
||||
|
||||
export type ChatRequest = {
|
||||
@@ -128,7 +128,29 @@ export type ConfirmToolActionRequest = {
|
||||
sessionId: string;
|
||||
};
|
||||
|
||||
export type Content = RawTextContent | RawImageContent | RawEmbeddedResource | RawAudioContent | RawResource;
|
||||
export type Content = ({
|
||||
type: 'text';
|
||||
} & RawTextContent) | ({
|
||||
type: 'image';
|
||||
} & RawImageContent) | ({
|
||||
type: 'resource';
|
||||
} & RawEmbeddedResource) | ({
|
||||
type: 'audio';
|
||||
} & RawAudioContent) | ({
|
||||
type: 'resource_link';
|
||||
} & RawResource);
|
||||
|
||||
export type ContentBlock = ({
|
||||
type: 'text';
|
||||
} & RawTextContent) | ({
|
||||
type: 'image';
|
||||
} & RawImageContent) | ({
|
||||
type: 'resource';
|
||||
} & RawEmbeddedResource) | ({
|
||||
type: 'audio';
|
||||
} & RawAudioContent) | ({
|
||||
type: 'resource_link';
|
||||
} & RawResource);
|
||||
|
||||
export type Conversation = Array<Message>;
|
||||
|
||||
@@ -1112,7 +1134,7 @@ export type RetryConfig = {
|
||||
timeout_seconds?: number | null;
|
||||
};
|
||||
|
||||
export type Role = string;
|
||||
export type Role = 'user' | 'assistant';
|
||||
|
||||
export type RunNowResponse = {
|
||||
session_id: string;
|
||||
@@ -1317,7 +1339,7 @@ export type SystemNotificationContent = {
|
||||
|
||||
export type SystemNotificationType = 'thinkingMessage' | 'inlineMessage' | 'creditsExhausted';
|
||||
|
||||
export type TaskSupport = string;
|
||||
export type TaskSupport = 'forbidden' | 'optional' | 'required';
|
||||
|
||||
export type TelemetryEventRequest = {
|
||||
event_name: string;
|
||||
|
||||
@@ -39,7 +39,6 @@ import {
|
||||
McpAppToolCancelled,
|
||||
McpAppToolInput,
|
||||
McpAppToolInputPartial,
|
||||
McpAppToolResult,
|
||||
DimensionLayout,
|
||||
OnDisplayModeChange,
|
||||
SamplingCreateMessageParams,
|
||||
@@ -142,7 +141,7 @@ interface McpAppRendererProps {
|
||||
sessionId?: string | null;
|
||||
toolInput?: McpAppToolInput;
|
||||
toolInputPartial?: McpAppToolInputPartial;
|
||||
toolResult?: McpAppToolResult;
|
||||
toolResult?: CallToolResult;
|
||||
toolCancelled?: McpAppToolCancelled;
|
||||
append?: (text: string) => void;
|
||||
displayMode?: GooseDisplayMode;
|
||||
@@ -505,14 +504,13 @@ export default function McpAppRenderer({
|
||||
},
|
||||
});
|
||||
|
||||
// rmcp serializes Content with a `type` discriminator via #[serde(tag = "type")].
|
||||
// Our generated TS types don't reflect this, but the wire format matches CallToolResult.content.
|
||||
return {
|
||||
content: (response.data?.content || []) as unknown as CallToolResult['content'],
|
||||
isError: response.data?.is_error || false,
|
||||
structuredContent: response.data?.structured_content as
|
||||
isError: response.data?.isError || false,
|
||||
structuredContent: response.data?.structuredContent as
|
||||
| { [key: string]: unknown }
|
||||
| undefined,
|
||||
_meta: response.data?._meta as { [key: string]: unknown } | undefined,
|
||||
};
|
||||
},
|
||||
[sessionId, extensionName]
|
||||
@@ -685,17 +683,6 @@ export default function McpAppRenderer({
|
||||
effectiveDisplayModes,
|
||||
]);
|
||||
|
||||
const appToolResult = useMemo((): CallToolResult | undefined => {
|
||||
if (!toolResult) return undefined;
|
||||
// rmcp serializes Content with a `type` discriminator via #[serde(tag = "type")].
|
||||
// Our generated TS types don't reflect this, but the wire format matches CallToolResult.content.
|
||||
return {
|
||||
content: toolResult.content as unknown as CallToolResult['content'],
|
||||
structuredContent: toolResult.structuredContent as { [key: string]: unknown } | undefined,
|
||||
_meta: toolResult._meta,
|
||||
};
|
||||
}, [toolResult]);
|
||||
|
||||
const isToolCancelled = !!toolCancelled;
|
||||
const isError = state.status === 'error';
|
||||
const isReady = state.status === 'ready';
|
||||
@@ -736,7 +723,7 @@ export default function McpAppRenderer({
|
||||
toolInputPartial={toolInputPartial ? { arguments: toolInputPartial.arguments } : undefined}
|
||||
toolCancelled={isToolCancelled}
|
||||
hostContext={hostContext}
|
||||
toolResult={appToolResult}
|
||||
toolResult={toolResult}
|
||||
onOpenLink={handleOpenLink}
|
||||
onMessage={handleMessage}
|
||||
onCallTool={handleCallTool}
|
||||
|
||||
@@ -4,7 +4,6 @@ import type {
|
||||
McpUiToolCancelledNotification,
|
||||
McpUiDisplayMode,
|
||||
} from '@modelcontextprotocol/ext-apps/app-bridge';
|
||||
import type { Content } from '../../api';
|
||||
|
||||
/**
|
||||
* Space-separated sandbox tokens for iframe permissions.
|
||||
@@ -37,12 +36,6 @@ export type McpAppToolInputPartial = McpUiToolInputPartialNotification['params']
|
||||
|
||||
export type McpAppToolCancelled = McpUiToolCancelledNotification['params'];
|
||||
|
||||
export type McpAppToolResult = {
|
||||
content: Content[];
|
||||
structuredContent?: unknown;
|
||||
_meta?: { [key: string]: unknown };
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback fired when the display mode changes, either via user-initiated
|
||||
* host-side controls or app-initiated `ui/request-display-mode` changes.
|
||||
|
||||
@@ -17,7 +17,9 @@ import { ChevronRight, FlaskConical } from 'lucide-react';
|
||||
import { TooltipWrapper } from './settings/providers/subcomponents/buttons/TooltipWrapper';
|
||||
import MCPUIResourceRenderer from './MCPUIResourceRenderer';
|
||||
import { isUIResource } from '@mcp-ui/client';
|
||||
import { CallToolResponse, Content, EmbeddedResource } from '../api';
|
||||
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { CallToolResponse, ContentBlock, EmbeddedResource } from '../api';
|
||||
|
||||
import McpAppRenderer from './McpApps/McpAppRenderer';
|
||||
import ToolApprovalButtons from './ToolApprovalButtons';
|
||||
|
||||
@@ -64,7 +66,7 @@ interface ToolCallWithResponseProps {
|
||||
isApprovalClicked?: boolean;
|
||||
}
|
||||
|
||||
function getToolResultContent(toolResult: Record<string, unknown>): Content[] {
|
||||
function getToolResultContent(toolResult: Record<string, unknown>): ContentBlock[] {
|
||||
if (toolResult.status !== 'success') {
|
||||
return [];
|
||||
}
|
||||
@@ -75,8 +77,11 @@ function getToolResultContent(toolResult: Record<string, unknown>): Content[] {
|
||||
});
|
||||
}
|
||||
|
||||
function isEmbeddedResource(content: Content): content is EmbeddedResource {
|
||||
return 'resource' in content && typeof (content as Record<string, unknown>).resource === 'object';
|
||||
function isEmbeddedResource(
|
||||
content: ContentBlock
|
||||
): content is EmbeddedResource & { type: 'resource' } {
|
||||
const c = content as Record<string, unknown>;
|
||||
return c.type === 'resource' && typeof c.resource === 'object' && c.resource !== null;
|
||||
}
|
||||
|
||||
interface McpAppWrapperProps {
|
||||
@@ -120,7 +125,9 @@ function McpAppWrapper({
|
||||
|
||||
const resultWithMeta = toolResponse?.toolResult as ToolResultWithMeta | undefined;
|
||||
const toolResult =
|
||||
resultWithMeta?.status === 'success' && resultWithMeta.value ? resultWithMeta.value : undefined;
|
||||
resultWithMeta?.status === 'success' && resultWithMeta.value
|
||||
? (resultWithMeta.value as unknown as CallToolResult)
|
||||
: undefined;
|
||||
|
||||
if (!resourceUri) return null;
|
||||
if (requestWithMeta.toolCall.status !== 'success') return null;
|
||||
@@ -217,13 +224,11 @@ export default function ToolCallWithResponse({
|
||||
!hasMcpAppResourceURI &&
|
||||
toolResponse?.toolResult &&
|
||||
getToolResultContent(toolResponse.toolResult).map((content, index) => {
|
||||
const resourceContent = isEmbeddedResource(content)
|
||||
? { ...content, type: 'resource' as const }
|
||||
: null;
|
||||
if (resourceContent && isUIResource(resourceContent)) {
|
||||
if (!isEmbeddedResource(content)) return null;
|
||||
if (isUIResource(content)) {
|
||||
return (
|
||||
<div key={index} className="mt-3">
|
||||
<MCPUIResourceRenderer content={resourceContent} appendPromptToChat={append} />
|
||||
<MCPUIResourceRenderer content={content} appendPromptToChat={append} />
|
||||
<div className="mt-3 p-4 py-3 border border-border-primary rounded-lg bg-background-secondary flex items-center">
|
||||
<FlaskConical className="mr-2" size={20} />
|
||||
<div className="text-sm font-sans">
|
||||
@@ -857,21 +862,22 @@ interface ToolResultViewProps {
|
||||
name: string;
|
||||
arguments: Record<string, unknown>;
|
||||
};
|
||||
result: Content;
|
||||
result: ContentBlock;
|
||||
isStartExpanded: boolean;
|
||||
}
|
||||
|
||||
function ToolResultView({ toolCall, result, isStartExpanded }: ToolResultViewProps) {
|
||||
const hasText = (c: Content): c is Content & { text: string } =>
|
||||
const hasText = (c: ContentBlock): c is ContentBlock & { text: string } =>
|
||||
'text' in c && typeof (c as Record<string, unknown>).text === 'string';
|
||||
|
||||
const hasImage = (c: Content): c is Content & { data: string; mimeType: string } => {
|
||||
const hasImage = (c: ContentBlock): c is ContentBlock & { data: string; mimeType: string } => {
|
||||
if (!('data' in c && 'mimeType' in c)) return false;
|
||||
const mimeType = (c as Record<string, unknown>).mimeType;
|
||||
return typeof mimeType === 'string' && mimeType.startsWith('image');
|
||||
};
|
||||
|
||||
const hasResource = (c: Content): c is Content & { resource: unknown } => 'resource' in c;
|
||||
const hasResource = (c: ContentBlock): c is ContentBlock & { resource: unknown } =>
|
||||
'resource' in c;
|
||||
|
||||
const wrapMarkdown = (text: string): string => {
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user