Skip to content
Open
Show file tree
Hide file tree
Changes from all 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.length > 0 ? errors : []);
}

/**
* 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> {
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
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