Skip to content
Merged
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
4 changes: 3 additions & 1 deletion packages/agents-a365-runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,12 @@
},
"dependencies": {
"@microsoft/agents-hosting": "*",
"@azure/identity": "*"
"@azure/identity": "*",
"jsonwebtoken": "^9.0.2"
},
"devDependencies": {
"@types/jest": "^29.5.14",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^20.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { TurnContext, Authorization } from '@microsoft/agents-hosting';
import { getMcpPlatformAuthenticationScope } from './environment-utils';

export class AgenticAuthenticationService {
public static async GetAgenticUserToken(authorization: Authorization, turnContext: TurnContext) {
public static async GetAgenticUserToken(authorization: Authorization, authHandlerName: string, turnContext: TurnContext) {
const scope = getMcpPlatformAuthenticationScope();
return (await authorization.exchangeToken(turnContext, 'agentic', { scopes: [scope] })).token || '';
return (await authorization.exchangeToken(turnContext, authHandlerName, { scopes: [scope] })).token || '';
}
}
3 changes: 2 additions & 1 deletion packages/agents-a365-runtime/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './power-platform-api-discovery';
export * from './agentic-authorization-service';
export * from './environment-utils';
export * from './environment-utils';
export * from './utility';
50 changes: 50 additions & 0 deletions packages/agents-a365-runtime/src/utility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { TurnContext } from '@microsoft/agents-hosting';
import * as jwt from 'jsonwebtoken';

/**
* Utility class providing helper methods for agent runtime operations.
*/
export class Utility {
/**
* Decodes the current token and retrieves the App ID (appid or azp claim).
* @param token Token to Decode
* @returns AppId
*/
public static GetAppIdFromToken(token: string): string {
if (!token || token.trim() === '') {
return '00000000-0000-0000-0000-000000000000';
Comment thread
pontemonti marked this conversation as resolved.
}

try {
const decoded = jwt.decode(token) as jwt.JwtPayload;
if (!decoded) {
return '';
}

// Look for appid claim first, then azp claim as fallback
const appIdClaim = decoded['appid'] || decoded['azp'];
return appIdClaim || '';
} catch (_error) {
// Silent error handling - return empty string on decode failure
return '';
}
}

/**
* Resolves the agent identity from the turn context or auth token.
* @param context Turn Context of the turn.
* @param authToken Auth token if available.
* @returns Agent identity (App ID)
*/
public static ResolveAgentIdentity(context: TurnContext, authToken: string): string {
// App ID is required to pass to MCP server URL.
const agenticAppId = context.activity.isAgenticRequest()
? context.activity.getAgenticInstanceId() || ''
Comment thread
pontemonti marked this conversation as resolved.
: this.GetAppIdFromToken(authToken);

return agenticAppId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the MIT License.

import { McpToolServerConfigurationService, McpClientTool, Utility, MCPServerConfig } from '@microsoft/agents-a365-tooling';
import { AgenticAuthenticationService } from '@microsoft/agents-a365-runtime';
import { AgenticAuthenticationService, Utility as RuntimeUtility } from '@microsoft/agents-a365-runtime';

// Agents SDK
import { TurnContext, Authorization } from '@microsoft/agents-hosting';
Expand All @@ -20,11 +20,16 @@ export class McpToolRegistrationService {
/**
* Registers MCP tool servers and updates agent options with discovered tools and server configs.
* Call this to enable dynamic Claude tool access.
* @param agentOptions The Claude Agent options to which MCP servers will be added.
* @param authorization Authorization object for token exchange.
* @param authHandlerName The name of the auth handler to use for token exchange.
* @param turnContext The TurnContext of the current request.
* @param authToken Optional bearer token for MCP server access.
*/
async addToolServersToAgent(
agentOptions: Options,
agentUserId: string,
authorization: Authorization,
authHandlerName: string,
turnContext: TurnContext,
authToken: string
): Promise<void> {
Expand All @@ -34,14 +39,14 @@ export class McpToolRegistrationService {
}

if (!authToken) {
authToken = await AgenticAuthenticationService.GetAgenticUserToken(authorization, turnContext);
authToken = await AgenticAuthenticationService.GetAgenticUserToken(authorization, authHandlerName, turnContext);
}

// Validate the authentication token
Utility.ValidateAuthToken(authToken);

const servers = await this.configService.listToolServers(agentUserId, authToken);

const agenticAppId = RuntimeUtility.ResolveAgentIdentity(turnContext, authToken);
const servers = await this.configService.listToolServers(agenticAppId, authToken);
const mcpServers: Record<string, McpServerConfig> = {};
const tools: McpClientTool[] = [];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

// Microsoft Agent 365 SDK
import { McpToolServerConfigurationService, Utility } from '@microsoft/agents-a365-tooling';
import { AgenticAuthenticationService } from '@microsoft/agents-a365-runtime';
import { AgenticAuthenticationService, Utility as RuntimeUtility } from '@microsoft/agents-a365-runtime';

// Agents SDK
import { TurnContext, Authorization } from '@microsoft/agents-hosting';
Expand All @@ -22,11 +22,17 @@ export class McpToolRegistrationService {
/**
* Registers MCP tool servers and updates agent options with discovered tools and server configs.
* Call this to enable dynamic LangChain tool access based on the current MCP environment.
* @param agent The LangChain Agent instance to which MCP servers will be added.
* @param authorization Authorization object for token exchange.
* @param authHandlerName The name of the auth handler to use for token exchange.
* @param turnContext The TurnContext of the current request.
* @param authToken Optional bearer token for MCP server access.
* @returns The updated Agent instance with registered MCP servers.
*/
async addToolServersToAgent(
agent: ReactAgent,
agentUserId: string,
authorization: Authorization,
authHandlerName: string,
turnContext: TurnContext,
authToken: string
): Promise<ReactAgent> {
Expand All @@ -36,14 +42,14 @@ export class McpToolRegistrationService {
}

if (!authToken) {
authToken = await AgenticAuthenticationService.GetAgenticUserToken(authorization, turnContext);
authToken = await AgenticAuthenticationService.GetAgenticUserToken(authorization, authHandlerName, turnContext);
}

// Validate the authentication token
Utility.ValidateAuthToken(authToken);

const servers = await this.configService.listToolServers(agentUserId, authToken);

const agenticAppId = RuntimeUtility.ResolveAgentIdentity(turnContext, authToken);
const servers = await this.configService.listToolServers(agenticAppId, authToken);
const mcpServers: Record<string, Connection> = {};

for (const server of servers) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the MIT License.

import { McpToolServerConfigurationService, Utility } from '@microsoft/agents-a365-tooling';
import { AgenticAuthenticationService } from '@microsoft/agents-a365-runtime';
import { AgenticAuthenticationService, Utility as RuntimeUtility } from '@microsoft/agents-a365-runtime';

// Agents SDK
import { TurnContext, Authorization } from '@microsoft/agents-hosting';
Expand All @@ -21,11 +21,17 @@ export class McpToolRegistrationService {
/**
* Registers MCP tool servers and updates agent options with discovered tools and server configs.
* Call this to enable dynamic OpenAI tool access based on the current MCP environment.
* @param agent The OpenAI Agent instance to which MCP servers will be added.
* @param authorization Authorization object for token exchange.
* @param authHandlerName The name of the auth handler to use for token exchange.
* @param turnContext The TurnContext of the current request.
* @param authToken Optional bearer token for MCP server access.
* @returns The updated Agent instance with registered MCP servers.
*/
async addToolServersToAgent(
agent: Agent,
agentUserId: string,
authorization: Authorization,
authHandlerName: string,
turnContext: TurnContext,
authToken: string
): Promise<Agent> {
Expand All @@ -35,13 +41,14 @@ export class McpToolRegistrationService {
}

if (!authToken) {
authToken = await AgenticAuthenticationService.GetAgenticUserToken(authorization, turnContext);
authToken = await AgenticAuthenticationService.GetAgenticUserToken(authorization, authHandlerName, turnContext);
}

// Validate the authentication token
Utility.ValidateAuthToken(authToken);
Comment thread
pontemonti marked this conversation as resolved.

const servers = await this.configService.listToolServers(agentUserId, authToken);
const agenticAppId = RuntimeUtility.ResolveAgentIdentity(turnContext, authToken);
const servers = await this.configService.listToolServers(agenticAppId, authToken);
const mcpServers: MCPServerStreamableHttp[] = [];

for (const server of servers) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ export class McpToolServerConfigurationService {
/**
* Return MCP server definitions for the given agent. In development (NODE_ENV=Development) this reads the local ToolingManifest.json; otherwise it queries the remote tooling gateway.
*
* @param agentUserId The unique identifier of the digital worker/agent user for which to discover servers.
* @param agenticAppId The agentic app id for which to discover servers.
* @param authToken Optional bearer token used when querying the remote tooling gateway.
* @returns A promise resolving to an array of normalized MCP server configuration objects.
*/
async listToolServers(agentUserId: string, authToken: string): Promise<MCPServerConfig[]> {
return await (this.isDevScenario() ? this.getMCPServerConfigsFromManifest() : this.getMCPServerConfigsFromToolingGateway(agentUserId, authToken));
async listToolServers(agenticAppId: string, authToken: string): Promise<MCPServerConfig[]> {
return await (this.isDevScenario() ? this.getMCPServerConfigsFromManifest() : this.getMCPServerConfigsFromToolingGateway(agenticAppId, authToken));
}

/**
Expand Down Expand Up @@ -72,15 +72,15 @@ export class McpToolServerConfigurationService {
* 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.
*
* @param agentId The digital worker/agent id used by the tooling gateway to scope results.
* @param agenticAppId The agentic app id used by the tooling gateway to scope results.
* @param authToken Optional Bearer token to include in the Authorization header when calling the gateway.
* @throws Error when the gateway call fails or returns an unexpected payload.
*/
private async getMCPServerConfigsFromToolingGateway(agentUserId: string, authToken: string): Promise<MCPServerConfig[]> {
private async getMCPServerConfigsFromToolingGateway(agenticAppId: string, authToken: string): Promise<MCPServerConfig[]> {
// Validate the authentication token
Utility.ValidateAuthToken(authToken);

const configEndpoint = Utility.GetToolingGatewayForDigitalWorker(agentUserId);
const configEndpoint = Utility.GetToolingGatewayForDigitalWorker(agenticAppId);

try {
const response = await axios.get(
Expand Down
16 changes: 8 additions & 8 deletions packages/agents-a365-tooling/src/Utility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,19 +66,19 @@ export class Utility {
}

/**
* Construct the tooling gateway URL for a given digital worker (agent user).
* This endpoint is used to discover MCP servers associated with the specified agent user.
* Construct the tooling gateway URL for a given agent identity.
* This endpoint is used to discover MCP servers associated with the specified agent identity.
*
* Example:
* Utility.GetToolingGatewayForDigitalWorker('agent-123')
* // => "https://agent365.svc.cloud.microsoft/agentGateway/agentApplicationInstances/agent-123/mcpServers"
* Utility.GetToolingGatewayForDigitalWorker(agenticAppId)
* // => "https://agent365.svc.cloud.microsoft/agents/{agenticAppId}/mcpServers"
*
* @param agentUserId - The unique identifier of the digital worker (agent user).
* @returns A fully-qualified URL pointing at the tooling gateway for the agent user.
* @param agenticAppId - The unique identifier for the agent identity.
* @returns A fully-qualified URL pointing at the tooling gateway for the agent.
*/
public static GetToolingGatewayForDigitalWorker(agentUserId: string): string {
public static GetToolingGatewayForDigitalWorker(agenticAppId: string): string {
// The endpoint needs to be updated based on the environment (prod, dev, etc.)
return `${this.getMcpPlatformBaseUrl()}/agents/${agentUserId}/mcpServers`;
return `${this.getMcpPlatformBaseUrl()}/agents/${agenticAppId}/mcpServers`;
}

/**
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ overrides:
# Zod
"zod": "^4.1.12"

# JSON Web Token
"jsonwebtoken": "^9.0.2"

# Development dependencies - align versions
"@microsoft/m365agentsplayground": "^0.2.18"
"typescript": "^5.6.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export class A365Agent extends AgentApplication<ApplicationTurnState> {
isApplicationInstalled: boolean = false;
termsAndConditionsAccepted: boolean = false;
agentName = 'A365 Agent';
authHandlerName = 'agentic';

constructor() {
super();
Expand Down Expand Up @@ -61,7 +62,7 @@ export class A365Agent extends AgentApplication<ApplicationTurnState> {
}

try {
const client = await getClient(this.getAuthorizationSafe(), turnContext);
const client = await getClient(this.getAuthorizationSafe(), this.authHandlerName, turnContext);
const response = await this.invokeAgent(client, userMessage);
await turnContext.sendActivity(response);
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface Client {
const toolService = new McpToolRegistrationService();
const localMcpService = new LocalMcpToolRegistrationService();

export async function getClient(authorization: Authorization | undefined, turnContext: TurnContext): Promise<Client> {
export async function getClient(authorization: Authorization | undefined, authHandlerName: string, turnContext: TurnContext): Promise<Client> {
const agent = new Agent({
// You can customize the agent configuration here if needed
name: 'OpenAI Agent',
Expand All @@ -37,8 +37,8 @@ export async function getClient(authorization: Authorization | undefined, turnCo
// Use production MCP service (auth required)
await toolService.addToolServersToAgent(
agent,
process.env.AGENTIC_USER_ID || '',
authorization,
authHandlerName,
turnContext,
process.env.MCP_AUTH_TOKEN || '',
);
Expand Down
Loading