From 367d31b6d86fcebe4f42c89cae61e3a9ee1a2344 Mon Sep 17 00:00:00 2001 From: Swapnil Nagar Date: Sun, 1 Feb 2026 01:17:37 -0800 Subject: [PATCH 1/5] Adding the McpResourceTrigger --- package-lock.json | 4 +- package.json | 2 +- src/app.ts | 12 + src/constants.ts | 2 +- .../toMcpResourceTriggerOptionsToRpc.ts | 56 ++++ src/trigger.ts | 18 ++ .../toMcpResourceTriggerOptionsToRpc.test.ts | 281 ++++++++++++++++++ types/app.d.ts | 8 + types/index.d.ts | 1 + types/mcpResource.d.ts | 125 ++++++++ types/trigger.d.ts | 7 + 11 files changed, 512 insertions(+), 4 deletions(-) create mode 100644 src/converters/toMcpResourceTriggerOptionsToRpc.ts create mode 100644 test/converters/toMcpResourceTriggerOptionsToRpc.test.ts create mode 100644 types/mcpResource.d.ts diff --git a/package-lock.json b/package-lock.json index d54728d5..f43d6fcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@azure/functions", - "version": "4.11.1", + "version": "4.12.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@azure/functions", - "version": "4.11.1", + "version": "4.12.0", "license": "MIT", "dependencies": { "@azure/functions-extensions-base": "0.2.0", diff --git a/package.json b/package.json index ef544a22..45c3ebb2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@azure/functions", - "version": "4.11.1", + "version": "4.12.0", "description": "Microsoft Azure Functions NodeJS Framework", "keywords": [ "azure", diff --git a/src/app.ts b/src/app.ts index 84d4be77..9659545b 100644 --- a/src/app.ts +++ b/src/app.ts @@ -11,6 +11,7 @@ import { HttpHandler, HttpMethod, HttpMethodFunctionOptions, + McpResourceFunctionOptions, McpToolFunctionOptions, MySqlFunctionOptions, ServiceBusQueueFunctionOptions, @@ -157,6 +158,17 @@ export function mcpTool(name: string, options: McpToolFunctionOptions): void { generic(name, convertToGenericOptions(options, trigger.mcpTool)); } +/** + * Registers an MCP Resource function in your app. + * This function is triggered when an MCP client reads the resource and allows you to define the resource content. + * + * @param name - The name of the function. This must be unique within your app and is primarily used for tracking purposes. + * @param options - Configuration options for the MCP Resource function, including the handler and trigger-specific settings. + */ +export function mcpResource(name: string, options: McpResourceFunctionOptions): void { + generic(name, convertToGenericOptions(options, trigger.mcpResource)); +} + export function generic(name: string, options: GenericFunctionOptions): void { if (!hasSetModel) { setProgrammingModel(); diff --git a/src/constants.ts b/src/constants.ts index b92e8d3a..d714882a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,6 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. -export const version = '4.11.1'; +export const version = '4.12.0'; export const returnBindingKey = '$return'; diff --git a/src/converters/toMcpResourceTriggerOptionsToRpc.ts b/src/converters/toMcpResourceTriggerOptionsToRpc.ts new file mode 100644 index 00000000..d6a82387 --- /dev/null +++ b/src/converters/toMcpResourceTriggerOptionsToRpc.ts @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. + +import { McpResourceTriggerOptions, McpResourceTriggerOptionsToRpc } from '../../types'; + +/** + * Converts an McpResourceTriggerOptions object to an McpResourceTriggerOptionsToRpc object. + * + * @param options - The input options to be converted. + * @returns The converted McpResourceTriggerOptionsToRpc object. + * @throws Error if required properties are missing or invalid. + */ +export function convertToMcpResourceTriggerOptionsToRpc( + options: McpResourceTriggerOptions +): McpResourceTriggerOptionsToRpc { + // Validate required properties + if (!options.uri || typeof options.uri !== 'string' || options.uri.trim() === '') { + throw new Error('MCP Resource trigger requires a valid "uri" property.'); + } + + if (!options.resourceName || typeof options.resourceName !== 'string' || options.resourceName.trim() === '') { + throw new Error('MCP Resource trigger requires a valid "resourceName" property.'); + } + + // Build the result object with required properties + const result: McpResourceTriggerOptionsToRpc = { + uri: options.uri, + resourceName: options.resourceName, + }; + + // Add optional properties if they are defined + if (options.title !== undefined) { + result.title = options.title; + } + + if (options.description !== undefined) { + result.description = options.description; + } + + if (options.mimeType !== undefined) { + result.mimeType = options.mimeType; + } + + if (options.size !== undefined) { + if (typeof options.size !== 'number' || options.size < 0) { + throw new Error('MCP Resource trigger "size" must be a non-negative number.'); + } + result.size = options.size; + } + + if (options.metadata !== undefined) { + result.metadata = options.metadata; + } + + return result; +} diff --git a/src/trigger.ts b/src/trigger.ts index 1a25f69b..90103e56 100644 --- a/src/trigger.ts +++ b/src/trigger.ts @@ -12,6 +12,8 @@ import { GenericTriggerOptions, HttpTrigger, HttpTriggerOptions, + McpResourceTrigger, + McpResourceTriggerOptions, McpToolTrigger, McpToolTriggerOptions, MySqlTrigger, @@ -34,6 +36,7 @@ import { WebPubSubTriggerOptions, } from '@azure/functions'; import { addBindingName } from './addBindingName'; +import { convertToMcpResourceTriggerOptionsToRpc } from './converters/toMcpResourceTriggerOptionsToRpc'; import { converToMcpToolTriggerOptionsToRpc } from './converters/toMcpToolTriggerOptionsToRpc'; export function http(options: HttpTriggerOptions): HttpTrigger { @@ -143,6 +146,21 @@ export function mcpTool(options: McpToolTriggerOptions): McpToolTrigger { }); } +/** + * Creates an MCP Resource trigger configuration. + * This function is used to define an MCP Resource trigger for an Azure Function. + * MCP Resources are read-only data sources that can be accessed by MCP clients. + * + * @param options - The configuration options for the MCP Resource trigger, including resource-specific metadata. + * @returns An MCP Resource trigger object with the specified configuration. + */ +export function mcpResource(options: McpResourceTriggerOptions): McpResourceTrigger { + return addTriggerBindingName({ + ...convertToMcpResourceTriggerOptionsToRpc(options), + type: 'mcpResourceTrigger', + }); +} + export function generic(options: GenericTriggerOptions): FunctionTrigger { return addTriggerBindingName(options); } diff --git a/test/converters/toMcpResourceTriggerOptionsToRpc.test.ts b/test/converters/toMcpResourceTriggerOptionsToRpc.test.ts new file mode 100644 index 00000000..398054e0 --- /dev/null +++ b/test/converters/toMcpResourceTriggerOptionsToRpc.test.ts @@ -0,0 +1,281 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. + +import 'mocha'; +import { expect } from 'chai'; +import { convertToMcpResourceTriggerOptionsToRpc } from '../../src/converters/toMcpResourceTriggerOptionsToRpc'; +import { McpResourceTriggerOptions } from '../../types/mcpResource'; + +describe('convertToMcpResourceTriggerOptionsToRpc', () => { + describe('required properties validation', () => { + it('should throw error when uri is missing', () => { + const input = { + resourceName: 'My Resource', + } as McpResourceTriggerOptions; + + expect(() => convertToMcpResourceTriggerOptionsToRpc(input)).to.throw( + 'MCP Resource trigger requires a valid "uri" property.' + ); + }); + + it('should throw error when uri is empty string', () => { + const input: McpResourceTriggerOptions = { + uri: '', + resourceName: 'My Resource', + }; + + expect(() => convertToMcpResourceTriggerOptionsToRpc(input)).to.throw( + 'MCP Resource trigger requires a valid "uri" property.' + ); + }); + + it('should throw error when uri is whitespace only', () => { + const input: McpResourceTriggerOptions = { + uri: ' ', + resourceName: 'My Resource', + }; + + expect(() => convertToMcpResourceTriggerOptionsToRpc(input)).to.throw( + 'MCP Resource trigger requires a valid "uri" property.' + ); + }); + + it('should throw error when resourceName is missing', () => { + const input = { + uri: 'mcp://example.com/resource', + } as McpResourceTriggerOptions; + + expect(() => convertToMcpResourceTriggerOptionsToRpc(input)).to.throw( + 'MCP Resource trigger requires a valid "resourceName" property.' + ); + }); + + it('should throw error when resourceName is empty string', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource', + resourceName: '', + }; + + expect(() => convertToMcpResourceTriggerOptionsToRpc(input)).to.throw( + 'MCP Resource trigger requires a valid "resourceName" property.' + ); + }); + + it('should throw error when resourceName is whitespace only', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource', + resourceName: ' ', + }; + + expect(() => convertToMcpResourceTriggerOptionsToRpc(input)).to.throw( + 'MCP Resource trigger requires a valid "resourceName" property.' + ); + }); + }); + + describe('minimal valid input', () => { + it('should convert with only required properties', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource', + resourceName: 'My Resource', + }; + + const result = convertToMcpResourceTriggerOptionsToRpc(input); + + expect(result.uri).to.equal('mcp://example.com/resource'); + expect(result.resourceName).to.equal('My Resource'); + expect(result.title).to.be.undefined; + expect(result.description).to.be.undefined; + expect(result.mimeType).to.be.undefined; + expect(result.size).to.be.undefined; + expect(result.metadata).to.be.undefined; + }); + }); + + describe('optional properties', () => { + it('should include title when provided', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource', + resourceName: 'My Resource', + title: 'Resource Title', + }; + + const result = convertToMcpResourceTriggerOptionsToRpc(input); + + expect(result.title).to.equal('Resource Title'); + }); + + it('should include description when provided', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource', + resourceName: 'My Resource', + description: 'This is a description of the resource.', + }; + + const result = convertToMcpResourceTriggerOptionsToRpc(input); + + expect(result.description).to.equal('This is a description of the resource.'); + }); + + it('should include mimeType when provided', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource', + resourceName: 'My Resource', + mimeType: 'application/json', + }; + + const result = convertToMcpResourceTriggerOptionsToRpc(input); + + expect(result.mimeType).to.equal('application/json'); + }); + + it('should include size when provided', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource', + resourceName: 'My Resource', + size: 1024, + }; + + const result = convertToMcpResourceTriggerOptionsToRpc(input); + + expect(result.size).to.equal(1024); + }); + + it('should include metadata when provided', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource', + resourceName: 'My Resource', + metadata: '{"key": "value"}', + }; + + const result = convertToMcpResourceTriggerOptionsToRpc(input); + + expect(result.metadata).to.equal('{"key": "value"}'); + }); + + it('should handle all optional properties together', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource/123', + resourceName: 'Complete Resource', + title: 'Complete Resource Title', + description: 'A complete resource with all properties.', + mimeType: 'text/plain', + size: 2048, + metadata: '{"version": "1.0", "author": "test"}', + }; + + const result = convertToMcpResourceTriggerOptionsToRpc(input); + + expect(result.uri).to.equal('mcp://example.com/resource/123'); + expect(result.resourceName).to.equal('Complete Resource'); + expect(result.title).to.equal('Complete Resource Title'); + expect(result.description).to.equal('A complete resource with all properties.'); + expect(result.mimeType).to.equal('text/plain'); + expect(result.size).to.equal(2048); + expect(result.metadata).to.equal('{"version": "1.0", "author": "test"}'); + }); + }); + + describe('size validation', () => { + it('should accept size of 0', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource', + resourceName: 'My Resource', + size: 0, + }; + + const result = convertToMcpResourceTriggerOptionsToRpc(input); + + expect(result.size).to.equal(0); + }); + + it('should throw error for negative size', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource', + resourceName: 'My Resource', + size: -1, + }; + + expect(() => convertToMcpResourceTriggerOptionsToRpc(input)).to.throw( + 'MCP Resource trigger "size" must be a non-negative number.' + ); + }); + + it('should accept large size values', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource', + resourceName: 'My Resource', + size: Number.MAX_SAFE_INTEGER, + }; + + const result = convertToMcpResourceTriggerOptionsToRpc(input); + + expect(result.size).to.equal(Number.MAX_SAFE_INTEGER); + }); + }); + + describe('edge cases', () => { + it('should handle URIs with special characters', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource?query=value&other=123', + resourceName: 'Resource with Query', + }; + + const result = convertToMcpResourceTriggerOptionsToRpc(input); + + expect(result.uri).to.equal('mcp://example.com/resource?query=value&other=123'); + }); + + it('should handle resourceName with special characters', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource', + resourceName: 'Resource (Test) - v1.0', + }; + + const result = convertToMcpResourceTriggerOptionsToRpc(input); + + expect(result.resourceName).to.equal('Resource (Test) - v1.0'); + }); + + it('should handle empty string for optional properties', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource', + resourceName: 'My Resource', + title: '', + description: '', + mimeType: '', + metadata: '', + }; + + const result = convertToMcpResourceTriggerOptionsToRpc(input); + + expect(result.title).to.equal(''); + expect(result.description).to.equal(''); + expect(result.mimeType).to.equal(''); + expect(result.metadata).to.equal(''); + }); + + it('should handle complex metadata JSON', () => { + const metadataObj = { + version: '2.0', + tags: ['tag1', 'tag2', 'tag3'], + nested: { + key: 'value', + number: 42, + }, + }; + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource', + resourceName: 'My Resource', + metadata: JSON.stringify(metadataObj), + }; + + const result = convertToMcpResourceTriggerOptionsToRpc(input); + + expect(result.metadata).to.equal(JSON.stringify(metadataObj)); + // Verify it can be parsed back + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expect(JSON.parse(result.metadata!)).to.deep.equal(metadataObj); + }); + }); +}); diff --git a/types/app.d.ts b/types/app.d.ts index b49952cd..a7361876 100644 --- a/types/app.d.ts +++ b/types/app.d.ts @@ -6,6 +6,7 @@ import { EventGridEvent, EventGridFunctionOptions } from './eventGrid'; import { EventHubFunctionOptions } from './eventHub'; import { GenericFunctionOptions } from './generic'; import { HttpFunctionOptions, HttpHandler, HttpMethodFunctionOptions } from './http'; +import { McpResourceFunctionOptions } from './mcpResource'; import { McpToolFunctionOptions } from './mcpTool'; import { MySqlFunctionOptions } from './mySql'; import { ServiceBusQueueFunctionOptions, ServiceBusTopicFunctionOptions } from './serviceBus'; @@ -204,4 +205,11 @@ export function webPubSub(name: string, options: WebPubSubFunctionO */ export function mcpTool(name: string, options: McpToolFunctionOptions): void; +/** + * Registers an MCP Resource function in your app that can be read by MCP clients. + * @param name The name of the function. The name must be unique within your app and will mostly be used for your own tracking purposes + * @param options Configuration options describing the inputs, outputs, and handler for this function + */ +export function mcpResource(name: string, options: McpResourceFunctionOptions): void; + export * as hook from './hooks/registerHook'; diff --git a/types/index.d.ts b/types/index.d.ts index c25dbc5d..b45bb766 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -17,6 +17,7 @@ export * from './hooks/logHooks'; export * from './http'; export * as input from './input'; export * from './InvocationContext'; +export * from './mcpResource'; export * from './mcpTool'; export * from './mySql'; export * as output from './output'; diff --git a/types/mcpResource.d.ts b/types/mcpResource.d.ts new file mode 100644 index 00000000..2179ea39 --- /dev/null +++ b/types/mcpResource.d.ts @@ -0,0 +1,125 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. + +import { FunctionOptions, FunctionResult, FunctionTrigger } from './index'; +import { InvocationContext } from './InvocationContext'; + +/** + * A handler function for MCP Resource triggers. + * + * @param messages - The messages or data received by the trigger. + * @param context - The invocation context for the function. + * @returns A result that can be a promise or a synchronous value. + */ +export type McpResourceTriggerHandler = (messages: T, context: InvocationContext) => FunctionResult; + +/** + * Configuration options for an MCP Resource function. + * This includes trigger-specific options and general function options. + */ +export interface McpResourceFunctionOptions extends McpResourceTriggerOptions, Partial { + /** + * The handler function to execute when the trigger is invoked. + */ + handler: McpResourceTriggerHandler; + + /** + * The trigger configuration for the MCP Resource. + */ + trigger?: McpResourceTrigger; +} + +/** + * Configuration options for an MCP Resource trigger. + * These options define the behavior and metadata for the trigger. + */ +export interface McpResourceTriggerOptions { + /** + * Unique URI identifier for the resource (must be absolute). + * This is the canonical identifier used by MCP clients to reference the resource. + */ + uri: string; + + /** + * Human-readable name of the resource. + * This name is displayed to users in MCP client interfaces. + */ + resourceName: string; + + /** + * Optional title for display purposes. + * A more descriptive title that can be shown in UI elements. + */ + title?: string; + + /** + * Description of the resource. + * Provides additional context about what the resource represents. + */ + description?: string; + + /** + * MIME type of the resource content. + * Specifies the format of the resource data (e.g., 'application/json', 'text/plain'). + */ + mimeType?: string; + + /** + * Optional size in bytes. + * The expected size of the resource content, if known. + */ + size?: number; + + /** + * JSON-serialized metadata object. + * Additional metadata about the resource in JSON format. + */ + metadata?: string; +} + +/** + * Configuration options for an MCP Resource trigger as sent to RPC. + * These options define the behavior and metadata for the trigger. + */ +export interface McpResourceTriggerOptionsToRpc { + /** + * Unique URI identifier for the resource (must be absolute). + */ + uri: string; + + /** + * Human-readable name of the resource. + */ + resourceName: string; + + /** + * Optional title for display purposes. + */ + title?: string; + + /** + * Description of the resource. + */ + description?: string; + + /** + * MIME type of the resource content. + */ + mimeType?: string; + + /** + * Optional size in bytes. + */ + size?: number; + + /** + * JSON-serialized metadata object. + */ + metadata?: string; +} + +/** + * Represents an MCP Resource trigger, combining base function trigger options + * with MCP Resource-specific trigger options. + */ +export type McpResourceTrigger = FunctionTrigger & McpResourceTriggerOptionsToRpc; diff --git a/types/trigger.d.ts b/types/trigger.d.ts index e8105d29..c91a9580 100644 --- a/types/trigger.d.ts +++ b/types/trigger.d.ts @@ -7,6 +7,7 @@ import { EventHubTrigger, EventHubTriggerOptions } from './eventHub'; import { GenericTriggerOptions } from './generic'; import { HttpTrigger, HttpTriggerOptions } from './http'; import { FunctionTrigger } from './index'; +import { McpResourceFunctionOptions, McpResourceTrigger } from './mcpResource'; import { McpToolFunctionOptions, McpToolTrigger } from './mcpTool'; import { MySqlTrigger, MySqlTriggerOptions } from './mySql'; import { @@ -96,6 +97,12 @@ export function webPubSub(options: WebPubSubTriggerOptions): WebPubSubTrigger; */ export function mcpTool(options: McpToolFunctionOptions): McpToolTrigger; +/** + * Creates an MCP Resource trigger for defining resources that can be read by MCP clients. + * [Link to docs and examples](//TODO Add link to docs and examples) + */ +export function mcpResource(options: McpResourceFunctionOptions): McpResourceTrigger; + /** * A generic option that can be used for any trigger type * Use this method if your desired trigger type does not already have its own method From fd82b1e7b423c4e65dbb12d1dd029a7686fc085f Mon Sep 17 00:00:00 2001 From: Swapnil Nagar Date: Mon, 2 Feb 2026 17:06:22 -0800 Subject: [PATCH 2/5] Adding the JSON validation for metadata --- .../toMcpResourceTriggerOptionsToRpc.ts | 8 + .../toMcpResourceTriggerOptionsToRpc.test.ts | 146 ++++++++++++++++++ 2 files changed, 154 insertions(+) diff --git a/src/converters/toMcpResourceTriggerOptionsToRpc.ts b/src/converters/toMcpResourceTriggerOptionsToRpc.ts index d6a82387..b425962b 100644 --- a/src/converters/toMcpResourceTriggerOptionsToRpc.ts +++ b/src/converters/toMcpResourceTriggerOptionsToRpc.ts @@ -49,6 +49,14 @@ export function convertToMcpResourceTriggerOptionsToRpc( } if (options.metadata !== undefined) { + // Validate that metadata is a valid JSON string + if (typeof options.metadata === 'string' && options.metadata.trim() !== '') { + try { + JSON.parse(options.metadata); + } catch (e) { + throw new Error('MCP Resource trigger "metadata" must be a valid JSON string.'); + } + } result.metadata = options.metadata; } diff --git a/test/converters/toMcpResourceTriggerOptionsToRpc.test.ts b/test/converters/toMcpResourceTriggerOptionsToRpc.test.ts index 398054e0..77e6ad90 100644 --- a/test/converters/toMcpResourceTriggerOptionsToRpc.test.ts +++ b/test/converters/toMcpResourceTriggerOptionsToRpc.test.ts @@ -176,6 +176,152 @@ describe('convertToMcpResourceTriggerOptionsToRpc', () => { }); }); + describe('metadata validation', () => { + it('should throw error for invalid JSON in metadata', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource', + resourceName: 'My Resource', + metadata: '{invalid json}', + }; + + expect(() => convertToMcpResourceTriggerOptionsToRpc(input)).to.throw( + 'MCP Resource trigger "metadata" must be a valid JSON string.' + ); + }); + + it('should throw error for malformed JSON with missing quotes', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource', + resourceName: 'My Resource', + metadata: '{key: value}', + }; + + expect(() => convertToMcpResourceTriggerOptionsToRpc(input)).to.throw( + 'MCP Resource trigger "metadata" must be a valid JSON string.' + ); + }); + + it('should throw error for JSON with trailing comma', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource', + resourceName: 'My Resource', + metadata: '{"key": "value",}', + }; + + expect(() => convertToMcpResourceTriggerOptionsToRpc(input)).to.throw( + 'MCP Resource trigger "metadata" must be a valid JSON string.' + ); + }); + + it('should throw error for plain text metadata', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource', + resourceName: 'My Resource', + metadata: 'not a json string', + }; + + expect(() => convertToMcpResourceTriggerOptionsToRpc(input)).to.throw( + 'MCP Resource trigger "metadata" must be a valid JSON string.' + ); + }); + + it('should accept valid JSON object string', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource', + resourceName: 'My Resource', + metadata: '{"key": "value"}', + }; + + const result = convertToMcpResourceTriggerOptionsToRpc(input); + + expect(result.metadata).to.equal('{"key": "value"}'); + }); + + it('should accept valid JSON array string', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource', + resourceName: 'My Resource', + metadata: '[1, 2, 3]', + }; + + const result = convertToMcpResourceTriggerOptionsToRpc(input); + + expect(result.metadata).to.equal('[1, 2, 3]'); + }); + + it('should accept valid JSON primitive string', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource', + resourceName: 'My Resource', + metadata: '"a simple string"', + }; + + const result = convertToMcpResourceTriggerOptionsToRpc(input); + + expect(result.metadata).to.equal('"a simple string"'); + }); + + it('should accept valid JSON number string', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource', + resourceName: 'My Resource', + metadata: '42', + }; + + const result = convertToMcpResourceTriggerOptionsToRpc(input); + + expect(result.metadata).to.equal('42'); + }); + + it('should accept valid JSON boolean string', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource', + resourceName: 'My Resource', + metadata: 'true', + }; + + const result = convertToMcpResourceTriggerOptionsToRpc(input); + + expect(result.metadata).to.equal('true'); + }); + + it('should accept valid JSON null string', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource', + resourceName: 'My Resource', + metadata: 'null', + }; + + const result = convertToMcpResourceTriggerOptionsToRpc(input); + + expect(result.metadata).to.equal('null'); + }); + + it('should accept empty string for metadata without validation', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource', + resourceName: 'My Resource', + metadata: '', + }; + + const result = convertToMcpResourceTriggerOptionsToRpc(input); + + expect(result.metadata).to.equal(''); + }); + + it('should accept whitespace-only string for metadata without validation', () => { + const input: McpResourceTriggerOptions = { + uri: 'mcp://example.com/resource', + resourceName: 'My Resource', + metadata: ' ', + }; + + const result = convertToMcpResourceTriggerOptionsToRpc(input); + + expect(result.metadata).to.equal(' '); + }); + }); + describe('size validation', () => { it('should accept size of 0', () => { const input: McpResourceTriggerOptions = { From d30a3a25b87f9ad00cc715440758ae769f8b1a1a Mon Sep 17 00:00:00 2001 From: Swapnil Nagar Date: Mon, 2 Feb 2026 17:12:22 -0800 Subject: [PATCH 3/5] Update Version --- package-lock.json | 4 ++-- package.json | 2 +- src/constants.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index f43d6fcc..bf288ee6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@azure/functions", - "version": "4.12.0", + "version": "4.12.0-preview.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@azure/functions", - "version": "4.12.0", + "version": "4.12.0-preview.1", "license": "MIT", "dependencies": { "@azure/functions-extensions-base": "0.2.0", diff --git a/package.json b/package.json index 45c3ebb2..b5fdebb0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@azure/functions", - "version": "4.12.0", + "version": "4.12.0-preview.1", "description": "Microsoft Azure Functions NodeJS Framework", "keywords": [ "azure", diff --git a/src/constants.ts b/src/constants.ts index d714882a..6e023410 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,6 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. -export const version = '4.12.0'; +export const version = '4.12.0-preview.1'; export const returnBindingKey = '$return'; From d5324b3d6fa50ec40e6491745bf1d814102484a1 Mon Sep 17 00:00:00 2001 From: Swapnil Nagar Date: Wed, 4 Feb 2026 11:28:19 -0800 Subject: [PATCH 4/5] Add the metadata support for the mcpToolTrigger --- .../toMcpToolTriggerOptionsToRpc.ts | 17 +- .../toMcpToolTriggerOptionsToRpc.test.ts | 183 ++++++++++++++++++ types/mcpTool.d.ts | 12 ++ 3 files changed, 211 insertions(+), 1 deletion(-) diff --git a/src/converters/toMcpToolTriggerOptionsToRpc.ts b/src/converters/toMcpToolTriggerOptionsToRpc.ts index c3025e43..607a8353 100644 --- a/src/converters/toMcpToolTriggerOptionsToRpc.ts +++ b/src/converters/toMcpToolTriggerOptionsToRpc.ts @@ -39,12 +39,27 @@ export function converToMcpToolTriggerOptionsToRpc( normalizedProperties = undefined; } + // Validate metadata if provided + if (mcpToolTriggerOptions.metadata !== undefined) { + if (typeof mcpToolTriggerOptions.metadata === 'string' && mcpToolTriggerOptions.metadata.trim() !== '') { + try { + JSON.parse(mcpToolTriggerOptions.metadata); + } catch (e) { + throw new Error('MCP Tool trigger "metadata" must be a valid JSON string.'); + } + } + } + // If we successfully normalized the properties, use them if (normalizedProperties !== undefined) { - return { + const result: McpToolTriggerOptionsToRpc = { ...baseResult, toolProperties: JSON.stringify(normalizedProperties), }; + if (mcpToolTriggerOptions.metadata !== undefined) { + result.metadata = mcpToolTriggerOptions.metadata; + } + return result; } // Handle cases where toolProperties is not an array diff --git a/test/converters/toMcpToolTriggerOptionsToRpc.test.ts b/test/converters/toMcpToolTriggerOptionsToRpc.test.ts index 99e3c517..c3608c17 100644 --- a/test/converters/toMcpToolTriggerOptionsToRpc.test.ts +++ b/test/converters/toMcpToolTriggerOptionsToRpc.test.ts @@ -489,4 +489,187 @@ describe('converToMcpToolTriggerOptionsToRpc', () => { expect(() => JSON.parse(result.toolProperties || '')).to.not.throw(); }); }); + + describe('metadata validation', () => { + it('should throw error for invalid JSON in metadata', () => { + const input: McpToolTriggerOptions = { + toolName: 'test-tool', + description: 'A test tool', + toolProperties: [], + metadata: '{invalid json}', + }; + + expect(() => converToMcpToolTriggerOptionsToRpc(input)).to.throw( + 'MCP Tool trigger "metadata" must be a valid JSON string.' + ); + }); + + it('should throw error for malformed JSON with missing quotes', () => { + const input: McpToolTriggerOptions = { + toolName: 'test-tool', + description: 'A test tool', + toolProperties: [], + metadata: '{key: value}', + }; + + expect(() => converToMcpToolTriggerOptionsToRpc(input)).to.throw( + 'MCP Tool trigger "metadata" must be a valid JSON string.' + ); + }); + + it('should throw error for JSON with trailing comma', () => { + const input: McpToolTriggerOptions = { + toolName: 'test-tool', + description: 'A test tool', + toolProperties: [], + metadata: '{"key": "value",}', + }; + + expect(() => converToMcpToolTriggerOptionsToRpc(input)).to.throw( + 'MCP Tool trigger "metadata" must be a valid JSON string.' + ); + }); + + it('should throw error for plain text metadata', () => { + const input: McpToolTriggerOptions = { + toolName: 'test-tool', + description: 'A test tool', + toolProperties: [], + metadata: 'not a json string', + }; + + expect(() => converToMcpToolTriggerOptionsToRpc(input)).to.throw( + 'MCP Tool trigger "metadata" must be a valid JSON string.' + ); + }); + + it('should accept valid JSON object string', () => { + const input: McpToolTriggerOptions = { + toolName: 'test-tool', + description: 'A test tool', + toolProperties: [], + metadata: '{"key": "value"}', + }; + + const result = converToMcpToolTriggerOptionsToRpc(input); + + expect(result.metadata).to.equal('{"key": "value"}'); + }); + + it('should accept valid JSON array string', () => { + const input: McpToolTriggerOptions = { + toolName: 'test-tool', + description: 'A test tool', + toolProperties: [], + metadata: '[1, 2, 3]', + }; + + const result = converToMcpToolTriggerOptionsToRpc(input); + + expect(result.metadata).to.equal('[1, 2, 3]'); + }); + + it('should accept valid JSON primitive string', () => { + const input: McpToolTriggerOptions = { + toolName: 'test-tool', + description: 'A test tool', + toolProperties: [], + metadata: '"a simple string"', + }; + + const result = converToMcpToolTriggerOptionsToRpc(input); + + expect(result.metadata).to.equal('"a simple string"'); + }); + + it('should accept valid JSON number string', () => { + const input: McpToolTriggerOptions = { + toolName: 'test-tool', + description: 'A test tool', + toolProperties: [], + metadata: '42', + }; + + const result = converToMcpToolTriggerOptionsToRpc(input); + + expect(result.metadata).to.equal('42'); + }); + + it('should accept valid JSON boolean string', () => { + const input: McpToolTriggerOptions = { + toolName: 'test-tool', + description: 'A test tool', + toolProperties: [], + metadata: 'true', + }; + + const result = converToMcpToolTriggerOptionsToRpc(input); + + expect(result.metadata).to.equal('true'); + }); + + it('should accept valid JSON null string', () => { + const input: McpToolTriggerOptions = { + toolName: 'test-tool', + description: 'A test tool', + toolProperties: [], + metadata: 'null', + }; + + const result = converToMcpToolTriggerOptionsToRpc(input); + + expect(result.metadata).to.equal('null'); + }); + + it('should accept empty string for metadata without validation', () => { + const input: McpToolTriggerOptions = { + toolName: 'test-tool', + description: 'A test tool', + toolProperties: [], + metadata: '', + }; + + const result = converToMcpToolTriggerOptionsToRpc(input); + + expect(result.metadata).to.equal(''); + }); + + it('should accept whitespace-only string for metadata without validation', () => { + const input: McpToolTriggerOptions = { + toolName: 'test-tool', + description: 'A test tool', + toolProperties: [], + metadata: ' ', + }; + + const result = converToMcpToolTriggerOptionsToRpc(input); + + expect(result.metadata).to.equal(' '); + }); + + it('should not include metadata in result when undefined', () => { + const input: McpToolTriggerOptions = { + toolName: 'test-tool', + description: 'A test tool', + toolProperties: [], + }; + + const result = converToMcpToolTriggerOptionsToRpc(input); + + expect(result.metadata).to.be.undefined; + }); + + it('should accept complex nested JSON object', () => { + const input: McpToolTriggerOptions = { + toolName: 'test-tool', + description: 'A test tool', + toolProperties: [], + metadata: '{"nested": {"key": "value"}, "array": [1, 2, 3], "boolean": true}', + }; + + const result = converToMcpToolTriggerOptionsToRpc(input); + + expect(result.metadata).to.equal('{"nested": {"key": "value"}, "array": [1, 2, 3], "boolean": true}'); + }); + }); }); diff --git a/types/mcpTool.d.ts b/types/mcpTool.d.ts index 7f4eedf6..c2b3cdd8 100644 --- a/types/mcpTool.d.ts +++ b/types/mcpTool.d.ts @@ -51,6 +51,12 @@ export interface McpToolTriggerOptions { * Can be provided as an array or as a Args object format. */ toolProperties?: McpToolProperty[] | Args; + + /** + * JSON-serialized metadata object. + * Additional metadata about the tool in JSON format. + */ + metadata?: string; } /** @@ -75,6 +81,12 @@ export interface McpToolTriggerOptionsToRpc { * This is a dictionary of key-value pairs that can be used to configure the trigger. */ toolProperties?: string; + + /** + * JSON-serialized metadata object. + * Additional metadata about the tool in JSON format. + */ + metadata?: string; } /** From 909da03140191e30a92f354a2b581af80f523c55 Mon Sep 17 00:00:00 2001 From: Swapnil Nagar Date: Wed, 4 Feb 2026 15:55:41 -0800 Subject: [PATCH 5/5] Version Update --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index bf288ee6..fc5f1a49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@azure/functions", - "version": "4.12.0-preview.1", + "version": "4.12.0-preview.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@azure/functions", - "version": "4.12.0-preview.1", + "version": "4.12.0-preview.2", "license": "MIT", "dependencies": { "@azure/functions-extensions-base": "0.2.0", diff --git a/package.json b/package.json index b5fdebb0..ad570ea5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@azure/functions", - "version": "4.12.0-preview.1", + "version": "4.12.0-preview.2", "description": "Microsoft Azure Functions NodeJS Framework", "keywords": [ "azure",