@azure/functions
Version:
Microsoft Azure Functions NodeJS Framework
109 lines (89 loc) • 4.32 kB
text/typescript
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.
import type { InvocationContext, McpToolResult } from '@azure/functions';
import { McpContentBlock, McpTextContent, McpToolResponse } from '../mcp/McpToolResponse';
import { warnIfLooksLikeMcpSdkValue } from '../mcp/sdkCompat';
import { shouldCreateStructuredContentMarker } from '../utils/mcpContentMarker';
const multiContentResultType = 'multi_content_result';
const textContentResultType = 'text';
/**
* Converts a tool handler's return value into the wire-format MCP tool result.
*
* Accepted inputs:
* - `null` / `undefined` → passed through.
* - `McpToolResponse` instance → serialized as-is.
* - `McpContentBlock` instance → wrapped in a single-block response.
* - Non-empty array of `McpContentBlock` instances → wrapped in a multi-block response.
* - Any other value → serialized as a text block. If the value's class is marked with
* `@McpContent`, it is also emitted as `structuredContent`.
*
* Detection uses `instanceof` exclusively — arbitrary user objects that happen to have a
* `content`/`type`/`structuredContent` field are treated as plain values.
*
* @param context Optional `InvocationContext` used to surface a one-time warning when the
* value looks like an `@modelcontextprotocol/sdk` response that is not auto-converted.
*/
export function toMcpToolResult(result: unknown, context?: InvocationContext): McpToolResult | null | undefined {
if (result === null || result === undefined) {
return result;
}
if (result instanceof McpToolResponse) {
return serializeToolResponse(result);
}
if (result instanceof McpContentBlock) {
return serializeToolResponse(new McpToolResponse({ content: [result] }));
}
if (Array.isArray(result) && result.length > 0 && result.every((b) => b instanceof McpContentBlock)) {
return serializeToolResponse(new McpToolResponse({ content: result as McpContentBlock[] }));
}
// Plain-value path: warn once if the value looks like an MCP SDK shape that
// the user likely intended to be converted. Behavior is unchanged.
warnIfLooksLikeMcpSdkValue(result, context);
const text = typeof result === 'string' ? result : JSON.stringify(result);
const mcpResult: McpToolResult = {
type: textContentResultType,
content: JSON.stringify({ type: textContentResultType, text }),
};
if (shouldCreateStructuredContentMarker(result)) {
mcpResult.structuredContent = JSON.stringify(result);
}
return mcpResult;
}
function serializeToolResponse(response: McpToolResponse): McpToolResult {
const blocks = ensureTextBlockWhenStructured(response);
let type: string;
let contentStr: string;
if (blocks.length === 1) {
const [block] = blocks as [McpContentBlock];
type = block.type;
contentStr = JSON.stringify(block);
} else {
type = multiContentResultType;
contentStr = JSON.stringify(blocks);
}
const out: McpToolResult = { type, content: contentStr };
if (response.structuredContent !== undefined && response.structuredContent !== null) {
out.structuredContent =
typeof response.structuredContent === 'string'
? response.structuredContent
: JSON.stringify(response.structuredContent);
}
return out;
}
/**
* Some MCP clients require a text content block alongside structured content for display.
* If the response declares `structuredContent` but has no `McpTextContent` block, synthesize one.
*/
function ensureTextBlockWhenStructured(response: McpToolResponse): McpContentBlock[] {
if (response.structuredContent === null || response.structuredContent === undefined) {
return response.content;
}
if (response.content.some((b) => b instanceof McpTextContent)) {
return response.content;
}
const fallbackText =
typeof response.structuredContent === 'string'
? response.structuredContent
: JSON.stringify(response.structuredContent);
return [...response.content, new McpTextContent(fallbackText)];
}