Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
"build:watch": "npm run build:watch --workspaces --if-present",
"clean": "npm run clean --workspaces --if-present && rimraf node_modules",
"test": "npm run test --workspaces --if-present -- --testPathIgnorePatterns=/integration/",
"test:coverage": "jest --config=tests/jest.config.cjs --coverage",
"test:coverage": "node --experimental-vm-modules ./node_modules/.bin/jest --config=tests/jest.config.cjs --coverage",
"test:watch": "npm run test:watch --workspaces --if-present",
"test:integration": "jest --config jest.integration.config.cjs",
"test:integration:watch": "jest --config jest.integration.config.cjs --watch",
"test:integration": "node --experimental-vm-modules ./node_modules/.bin/jest --config jest.integration.config.cjs",
"test:integration:watch": "node --experimental-vm-modules ./node_modules/.bin/jest --config jest.integration.config.cjs --watch",
"lint": "npm run lint --workspaces --if-present",
"lint:fix": "npm run lint:fix --workspaces --if-present",
"ci": "npm run ci --workspaces --if-present",
Expand Down
4 changes: 3 additions & 1 deletion packages/agents-a365-runtime/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export * from './power-platform-api-discovery';
export * from './agentic-authorization-service';
export * from './environment-utils';
export * from './utility';
export * from './utility';
export * from './operation-error';
export * from './operation-result';
38 changes: 38 additions & 0 deletions packages/agents-a365-runtime/src/operation-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/**
* Encapsulates an error from an operation.
*/
export class OperationError {
/**
* Gets the exception associated with the error.
*/
public readonly exception: Error;

/**
* Gets the message associated with the error.
*/
public get message(): string {
return this.exception.message;
}

/**
* Initializes a new instance of the OperationError class.
* @param exception The exception associated with the error.
*/
constructor(exception: Error) {
if (!exception) {
throw new Error('exception is required');
}
this.exception = exception;
}

/**
* Returns a string representation of the error.
* @returns A string representation of the error.
*/
public toString(): string {
return this.exception.toString();
}
}
66 changes: 66 additions & 0 deletions packages/agents-a365-runtime/src/operation-result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { OperationError } from './operation-error';

/**
* Represents the result of an operation.
*/
export class OperationResult {
private static readonly _success = new OperationResult(true);
private readonly _errors: OperationError[];

/**
* Gets a flag indicating whether the operation succeeded.
*/
public readonly succeeded: boolean;

/**
* Gets an array of OperationError instances indicating errors that occurred during the operation.
*/
public get errors(): OperationError[] {
return this._errors || [];
}

/**
* Private constructor for OperationResult.
* @param succeeded Whether the operation succeeded.
* @param errors Optional array of errors.
*/
private constructor(succeeded: boolean, errors?: OperationError[]) {
this.succeeded = succeeded;
this._errors = errors || [];
}

/**
* Returns an OperationResult indicating a successful operation.
*/
public static get success(): OperationResult {
return OperationResult._success;
}

/**
* Creates an OperationResult indicating a failed operation, with a list of errors if applicable.
* @param errors An optional array of OperationError which caused the operation to fail.
* @returns An OperationResult indicating a failed operation, with a list of errors if applicable.
*/
public static failed(...errors: OperationError[]): OperationResult {
return new OperationResult(false, errors && errors.length > 0 ? errors : []);
Comment thread
pontemonti marked this conversation as resolved.
Outdated
}

/**
* Converts the value of the current OperationResult object to its equivalent string representation.
* @returns A string representation of the current OperationResult object.
* @remarks
* If the operation was successful the toString() will return "Succeeded" otherwise it will return
* "Failed : " followed by a comma delimited list of error messages from its errors collection, if any.
*/
public toString(): string {
if (this.succeeded) {
return 'Succeeded';
}

const errorMessages = this.errors.map(e => e.message).join(', ');
return `Failed : ${errorMessages}`;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
import fs from 'fs';
import path from 'path';
import axios from 'axios';
import { TurnContext } from '@microsoft/agents-hosting';
import { OperationResult, OperationError } from '@microsoft/agents-a365-runtime';
import { MCPServerConfig, McpClientTool, ToolOptions } from './contracts';
import { ChatHistoryMessage, ChatMessageRequest } from './models/index';
import { Utility } from './Utility';

import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
Expand Down Expand Up @@ -81,6 +84,108 @@ export class McpToolServerConfigurationService {
return toolsObj.tools;
}

/**
* Sends chat history to the MCP platform for real-time threat protection.
*
* @param turnContext The turn context containing conversation information.
* @param chatHistoryMessages The chat history messages to send.
* @returns A Promise that resolves to an OperationResult indicating success or failure.
* @throws Error if turnContext or chatHistoryMessages is null/undefined.
* @throws Error if required turn context properties (Conversation.Id, Activity.Id, or Activity.Text) are null.
* @remarks
* HTTP exceptions (network errors, timeouts) are caught and logged but not rethrown.
* Instead, the method returns an OperationResult indicating whether the operation succeeded or failed.
* Callers can choose to inspect the result for error handling or ignore it if error details are not needed.
*/
async sendChatHistory(turnContext: TurnContext, chatHistoryMessages: ChatHistoryMessage[]): Promise<OperationResult>;

/**
* Sends chat history to the MCP platform for real-time threat protection.
*
* @param turnContext The turn context containing conversation information.
* @param chatHistoryMessages The chat history messages to send.
* @param options Optional tool options for sending chat history.
* @returns A Promise that resolves to an OperationResult indicating success or failure.
* @throws Error if turnContext or chatHistoryMessages is null/undefined.
* @throws Error if required turn context properties (Conversation.Id, Activity.Id, or Activity.Text) are null.
* @remarks
* HTTP exceptions (network errors, timeouts) are caught and logged but not rethrown.
* Instead, the method returns an OperationResult indicating whether the operation succeeded or failed.
* Callers can choose to inspect the result for error handling or ignore it if error details are not needed.
*/
async sendChatHistory(turnContext: TurnContext, chatHistoryMessages: ChatHistoryMessage[], options?: ToolOptions): Promise<OperationResult>;

async sendChatHistory(turnContext: TurnContext, chatHistoryMessages: ChatHistoryMessage[], options?: ToolOptions): Promise<OperationResult> {
Comment thread
pontemonti marked this conversation as resolved.
if (!turnContext) {
throw new Error('turnContext is required');
}
if (!chatHistoryMessages) {
throw new Error('chatHistoryMessages is required');
}

// Extract required information from turn context
const conversationId = turnContext.activity?.conversation?.id;
if (!conversationId) {
throw new Error('Conversation ID is required but not found in turn context');
}

const messageId = turnContext.activity?.id;
if (!messageId) {
throw new Error('Message ID is required but not found in turn context');
}

const userMessage = turnContext.activity?.text;
if (!userMessage) {
throw new Error('User message is required but not found in turn context');
}

// Get the endpoint URL
const endpoint = Utility.GetChatHistoryEndpoint();

this.logger.info(`Sending chat history to endpoint: ${endpoint}`);

// Create the request payload
const request: ChatMessageRequest = {
conversationId,
messageId,
userMessage,
chatHistory: chatHistoryMessages
};

try {
const headers = Utility.GetToolRequestHeaders(undefined, turnContext, options);

await axios.post(
endpoint,
request,
{
headers: {
...headers,
'Content-Type': 'application/json'
},
timeout: 10000 // 10 seconds timeout
}
);

this.logger.info('Successfully sent chat history to MCP platform');
return OperationResult.success;
} catch (err: unknown) {
const error = err as Error & { code?: string };

if (axios.isAxiosError(err)) {
if (err.code === 'ECONNABORTED' || err.code === 'ETIMEDOUT') {
this.logger.error(`Request timeout sending chat history to '${endpoint}': ${error.message}`);
} else {
this.logger.error(`HTTP error sending chat history to '${endpoint}': ${error.message}`);
}
} else {
this.logger.error(`Failed to send chat history to '${endpoint}': ${error.message}`);
}

return OperationResult.failed(new OperationError(error));
}
}

/**
* Query the tooling gateway for MCP servers for the specified agent and normalize each entry's mcpServerUniqueName into a full URL using Utility.BuildMcpServerUrl.
* Throws an error if the gateway call fails.
Expand Down
14 changes: 14 additions & 0 deletions packages/agents-a365-tooling/src/Utility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,18 @@ export class Utility {

return MCP_PLATFORM_PROD_BASE_URL;
}

/**
* Constructs the endpoint URL for sending chat history to the MCP platform for real-time threat protection.
*
* @returns An absolute URL that tooling components can use to send or retrieve chat messages for
* real-time threat protection scenarios.
* @remarks
* Call this method when constructing HTTP requests that need to access the chat-message history
* for real-time threat protection. The returned URL already includes the MCP platform base address
* and the fixed path segment `/agents/real-time-threat-protection/chat-message`.
*/
public static GetChatHistoryEndpoint(): string {
return `${this.getMcpPlatformBaseUrl()}/agents/real-time-threat-protection/chat-message`;
}
}
3 changes: 2 additions & 1 deletion packages/agents-a365-tooling/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './Utility';
export * from './McpToolServerConfigurationService';
export * from './contracts';
export * from './contracts';
export * from './models';
27 changes: 27 additions & 0 deletions packages/agents-a365-tooling/src/models/ChatHistoryMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/**
* Represents a single message in the chat history.
*/
export interface ChatHistoryMessage {
/**
* The unique identifier for the chat message.
*/
id: string;

/**
* The role of the message sender (e.g., "user", "assistant", "system").
*/
role: string;

/**
* The content of the chat message.
*/
content: string;

/**
* The timestamp of when the message was sent.
*/
timestamp: Date;
}
29 changes: 29 additions & 0 deletions packages/agents-a365-tooling/src/models/ChatMessageRequest.ts
Comment thread
pontemonti marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { ChatHistoryMessage } from './ChatHistoryMessage';

/**
* Represents the request payload for a real-time threat protection check on a chat message.
*/
export interface ChatMessageRequest {
/**
* The unique identifier for the conversation.
*/
conversationId: string;

/**
* The unique identifier for the message within the conversation.
*/
messageId: string;

/**
* The content of the user's message.
*/
userMessage: string;

/**
* The chat history messages.
*/
chatHistory: ChatHistoryMessage[];
}
5 changes: 5 additions & 0 deletions packages/agents-a365-tooling/src/models/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

export * from './ChatHistoryMessage';
export * from './ChatMessageRequest';
10 changes: 5 additions & 5 deletions tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"test": "jest --config jest.config.cjs --passWithNoTests --testPathIgnorePatterns=/integration/",
"test:watch": "jest --config jest.config.cjs --watch --testPathIgnorePatterns=/integration/",
"test:coverage": "jest --config jest.config.cjs --coverage --passWithNoTests --testPathIgnorePatterns=/integration/",
"test:verbose": "jest --config jest.config.cjs --verbose --passWithNoTests --testPathIgnorePatterns=/integration/",
"test:ci": "jest --config jest.config.cjs --coverage --ci --maxWorkers=2 --passWithNoTests --testPathIgnorePatterns=/integration/"
"test": "node --experimental-vm-modules $(npm root)/../node_modules/.bin/jest --config jest.config.cjs --passWithNoTests --testPathIgnorePatterns=/integration/",
"test:watch": "node --experimental-vm-modules $(npm root)/../node_modules/.bin/jest --config jest.config.cjs --watch --testPathIgnorePatterns=/integration/",
"test:coverage": "node --experimental-vm-modules $(npm root)/../node_modules/.bin/jest --config jest.config.cjs --coverage --passWithNoTests --testPathIgnorePatterns=/integration/",
"test:verbose": "node --experimental-vm-modules $(npm root)/../node_modules/.bin/jest --config jest.config.cjs --verbose --passWithNoTests --testPathIgnorePatterns=/integration/",
"test:ci": "node --experimental-vm-modules $(npm root)/../node_modules/.bin/jest --config jest.config.cjs --coverage --ci --maxWorkers=2 --passWithNoTests --testPathIgnorePatterns=/integration/"
},
"keywords": [
"agents",
Expand Down
Loading