@azure/functions
Version:
Microsoft Azure Functions NodeJS Framework
246 lines (223 loc) • 8.35 kB
text/typescript
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.
import type {
McpAudioContentInit as IAudioContentInit,
McpImageContentInit as IImageContentInit,
McpResourceContentInit as IResourceContentInit,
McpResourceLinkContentInit as IResourceLinkContentInit,
McpToolResponseInit as IMcpToolResponseInit,
} from '@azure/functions';
type BinaryData = string | Buffer | ArrayBuffer;
function normalizeBinaryData(data: BinaryData): string {
if (typeof data === 'string') {
return data;
}
if (Buffer.isBuffer(data)) {
return data.toString('base64');
}
if (ArrayBuffer.isView(data)) {
const view = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
return Buffer.from(view).toString('base64');
}
return Buffer.from(new Uint8Array(data)).toString('base64');
}
/**
* Base class for all MCP content blocks. The library uses `instanceof McpContentBlock`
* to discriminate content blocks from plain user values, so every content-block subclass
* must extend this class.
*
* ## Extending with custom content block types
*
* The library ships built-in subclasses for the MCP spec's current content block types
* (`McpTextContent`, `McpImageContent`, `McpAudioContent`, `McpResourceLinkContent`, `McpResourceContent`).
* If the spec adds a new block type — or your scenario needs a custom one — you can ship
* your own subclass without any library change. The converter only checks
* `instanceof McpContentBlock` and calls `JSON.stringify(block)`, which invokes your
* `toJSON()` to produce the wire payload.
*
* ### Example: adding a hypothetical `VideoContent`
*
* ```ts
* import { McpContentBlock } from '@azure/functions';
*
* export interface VideoContentInit {
* data: string | Buffer | ArrayBuffer;
* mimeType: string; // e.g. 'video/mp4'
* durationMs?: number; // optional field from a hypothetical spec
* }
*
* export class VideoContent extends McpContentBlock {
* readonly type = 'video' as const;
* readonly data: string | Buffer | ArrayBuffer;
* readonly mimeType: string;
* readonly durationMs?: number;
*
* constructor(init: VideoContentInit) {
* super();
* this.data = init.data;
* this.mimeType = init.mimeType;
* this.durationMs = init.durationMs;
* }
*
* toJSON(): Record<string, unknown> {
* const out: Record<string, unknown> = {
* type: this.type,
* data: toBase64(this.data),
* mimeType: this.mimeType,
* };
* if (this.durationMs !== undefined) out.durationMs = this.durationMs;
* return out;
* }
* }
*
* function toBase64(data: string | Buffer | ArrayBuffer): string {
* if (typeof data === 'string') return data;
* if (Buffer.isBuffer(data)) return data.toString('base64');
* return Buffer.from(new Uint8Array(data)).toString('base64');
* }
* ```
*
* ### Using a custom block from a tool handler
*
* Return it standalone, or mix it with built-in blocks inside an `McpToolResponse`:
*
* ```ts
* // Single block
* handler: async () => new VideoContent({ data: buf, mimeType: 'video/mp4' })
*
* // Mixed with built-ins + structured content
* handler: async () => new McpToolResponse({
* content: [
* new McpTextContent('Detected 3 scenes'),
* new VideoContent({ data: buf, mimeType: 'video/mp4' }),
* ],
* structuredContent: { scenes: 3, confidence: 0.92 },
* })
* ```
*
* ### What the library does for you
*
* - `instanceof McpContentBlock` treats your subclass identically to built-in blocks.
* - Single-block returns propagate your `type` string to the outer result; arrays are
* wrapped as `multi_content_result`.
* - `structuredContent` handling, fallback-text synthesis, and nullish passthrough all
* apply unchanged.
*
* ### What you must get right in your subclass
*
* - `toJSON()` must return the exact wire shape the spec requires for your `type`.
* - Binary payloads should be base64-encoded in `toJSON()` (see the `toBase64` helper
* above).
* - Plain object literals are **not** recognized — you must construct an instance.
*/
export abstract class McpContentBlock {
abstract readonly type: string;
/**
* Returns the wire representation of this block. Subclasses override to normalize
* binary payloads and omit undefined fields.
*/
abstract toJSON(): Record<string, unknown>;
}
export class McpTextContent extends McpContentBlock {
readonly type = 'text' as const;
readonly text: string;
constructor(text: string) {
super();
this.text = text;
}
toJSON(): Record<string, unknown> {
return { type: this.type, text: this.text };
}
}
export class McpImageContent extends McpContentBlock {
readonly type = 'image' as const;
readonly data: BinaryData;
readonly mimeType?: string;
constructor(init: IImageContentInit) {
super();
this.data = init.data;
this.mimeType = init.mimeType;
}
toJSON(): Record<string, unknown> {
const out: Record<string, unknown> = {
type: this.type,
data: normalizeBinaryData(this.data),
};
if (this.mimeType !== undefined) {
out.mimeType = this.mimeType;
}
return out;
}
}
export class McpAudioContent extends McpContentBlock {
readonly type = 'audio' as const;
readonly data: BinaryData;
readonly mimeType?: string;
constructor(init: IAudioContentInit) {
super();
this.data = init.data;
this.mimeType = init.mimeType;
}
toJSON(): Record<string, unknown> {
const out: Record<string, unknown> = {
type: this.type,
data: normalizeBinaryData(this.data),
};
if (this.mimeType !== undefined) {
out.mimeType = this.mimeType;
}
return out;
}
}
export class McpResourceLinkContent extends McpContentBlock {
readonly type = 'resource_link' as const;
readonly uri: string;
readonly name?: string;
readonly description?: string;
readonly mimeType?: string;
constructor(init: IResourceLinkContentInit) {
super();
this.uri = init.uri;
this.name = init.name;
this.description = init.description;
this.mimeType = init.mimeType;
}
toJSON(): Record<string, unknown> {
const out: Record<string, unknown> = { type: this.type, uri: this.uri };
if (this.name !== undefined) out.name = this.name;
if (this.description !== undefined) out.description = this.description;
if (this.mimeType !== undefined) out.mimeType = this.mimeType;
return out;
}
}
export class McpResourceContent extends McpContentBlock {
readonly type = 'resource' as const;
readonly resource: IResourceContentInit['resource'];
constructor(init: IResourceContentInit) {
super();
this.resource = init.resource;
}
toJSON(): Record<string, unknown> {
const r = this.resource;
const inner: Record<string, unknown> = { uri: r.uri };
if (r.mimeType !== undefined) inner.mimeType = r.mimeType;
if (r.text !== undefined) inner.text = r.text;
if (r.blob !== undefined) inner.blob = normalizeBinaryData(r.blob);
return { type: this.type, resource: inner };
}
}
/**
* A complete MCP tool response with explicit content blocks and optional structured content.
* Return an instance of this class from a tool handler when you need full control over
* both the content array and `structuredContent`.
*/
export class McpToolResponse {
readonly content: McpContentBlock[];
readonly structuredContent?: unknown;
readonly isError?: boolean;
constructor(init: IMcpToolResponseInit) {
this.content = init.content;
this.structuredContent = init.structuredContent;
this.isError = init.isError;
}
}