diff --git a/packages/agents-a365-runtime/package.json b/packages/agents-a365-runtime/package.json index b26657b9..ec83a455 100644 --- a/packages/agents-a365-runtime/package.json +++ b/packages/agents-a365-runtime/package.json @@ -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", diff --git a/packages/agents-a365-runtime/src/agentic-authorization-service.ts b/packages/agents-a365-runtime/src/agentic-authorization-service.ts index 6d09f550..9c8f2431 100644 --- a/packages/agents-a365-runtime/src/agentic-authorization-service.ts +++ b/packages/agents-a365-runtime/src/agentic-authorization-service.ts @@ -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 || ''; } } diff --git a/packages/agents-a365-runtime/src/index.ts b/packages/agents-a365-runtime/src/index.ts index feaef229..c6a9d973 100644 --- a/packages/agents-a365-runtime/src/index.ts +++ b/packages/agents-a365-runtime/src/index.ts @@ -1,3 +1,4 @@ export * from './power-platform-api-discovery'; export * from './agentic-authorization-service'; -export * from './environment-utils'; \ No newline at end of file +export * from './environment-utils'; +export * from './utility'; \ No newline at end of file diff --git a/packages/agents-a365-runtime/src/utility.ts b/packages/agents-a365-runtime/src/utility.ts new file mode 100644 index 00000000..0e7308cb --- /dev/null +++ b/packages/agents-a365-runtime/src/utility.ts @@ -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'; + } + + 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() || '' + : this.GetAppIdFromToken(authToken); + + return agenticAppId; + } +} \ No newline at end of file diff --git a/packages/agents-a365-tooling-extensions-claude/src/McpToolRegistrationService.ts b/packages/agents-a365-tooling-extensions-claude/src/McpToolRegistrationService.ts index b0743f67..4a22bf2d 100644 --- a/packages/agents-a365-tooling-extensions-claude/src/McpToolRegistrationService.ts +++ b/packages/agents-a365-tooling-extensions-claude/src/McpToolRegistrationService.ts @@ -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'; @@ -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 { @@ -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 = {}; const tools: McpClientTool[] = []; diff --git a/packages/agents-a365-tooling-extensions-langchain/src/McpToolRegistrationService.ts b/packages/agents-a365-tooling-extensions-langchain/src/McpToolRegistrationService.ts index f677ca08..8ef519cc 100644 --- a/packages/agents-a365-tooling-extensions-langchain/src/McpToolRegistrationService.ts +++ b/packages/agents-a365-tooling-extensions-langchain/src/McpToolRegistrationService.ts @@ -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'; @@ -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 { @@ -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 = {}; for (const server of servers) { diff --git a/packages/agents-a365-tooling-extensions-openai/src/McpToolRegistrationService.ts b/packages/agents-a365-tooling-extensions-openai/src/McpToolRegistrationService.ts index 9c5d3eaa..610f5517 100644 --- a/packages/agents-a365-tooling-extensions-openai/src/McpToolRegistrationService.ts +++ b/packages/agents-a365-tooling-extensions-openai/src/McpToolRegistrationService.ts @@ -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'; @@ -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 { @@ -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); - 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) { diff --git a/packages/agents-a365-tooling/src/McpToolServerConfigurationService.ts b/packages/agents-a365-tooling/src/McpToolServerConfigurationService.ts index 711b7c30..861fe137 100644 --- a/packages/agents-a365-tooling/src/McpToolServerConfigurationService.ts +++ b/packages/agents-a365-tooling/src/McpToolServerConfigurationService.ts @@ -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 { - return await (this.isDevScenario() ? this.getMCPServerConfigsFromManifest() : this.getMCPServerConfigsFromToolingGateway(agentUserId, authToken)); + async listToolServers(agenticAppId: string, authToken: string): Promise { + return await (this.isDevScenario() ? this.getMCPServerConfigsFromManifest() : this.getMCPServerConfigsFromToolingGateway(agenticAppId, authToken)); } /** @@ -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 { + private async getMCPServerConfigsFromToolingGateway(agenticAppId: string, authToken: string): Promise { // Validate the authentication token Utility.ValidateAuthToken(authToken); - const configEndpoint = Utility.GetToolingGatewayForDigitalWorker(agentUserId); + const configEndpoint = Utility.GetToolingGatewayForDigitalWorker(agenticAppId); try { const response = await axios.get( diff --git a/packages/agents-a365-tooling/src/Utility.ts b/packages/agents-a365-tooling/src/Utility.ts index c57d8561..3bebe22d 100644 --- a/packages/agents-a365-tooling/src/Utility.ts +++ b/packages/agents-a365-tooling/src/Utility.ts @@ -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`; } /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad256424..cf2b3c0c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,7 @@ overrides: '@anthropic-ai/claude-agent-sdk': ^0.1.30 express: ^5.1.0 zod: ^4.1.12 + jsonwebtoken: ^9.0.2 '@microsoft/m365agentsplayground': ^0.2.18 typescript: ^5.6.0 jest: ^29.7.0 @@ -200,10 +201,16 @@ importers: '@microsoft/agents-hosting': specifier: ^1.1.0-alpha.85 version: 1.1.0-alpha.85 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 devDependencies: '@types/jest': specifier: ^29.5.14 version: 29.5.14 + '@types/jsonwebtoken': + specifier: ^9.0.10 + version: 9.0.10 '@types/node': specifier: ^20.17.0 version: 20.19.25 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6f117105..2248a4db 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -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" diff --git a/tests-agent/openai-agent-auto-instrument-sample/src/A365Agent.ts b/tests-agent/openai-agent-auto-instrument-sample/src/A365Agent.ts index 183a2a81..fd02f8dc 100644 --- a/tests-agent/openai-agent-auto-instrument-sample/src/A365Agent.ts +++ b/tests-agent/openai-agent-auto-instrument-sample/src/A365Agent.ts @@ -16,6 +16,7 @@ export class A365Agent extends AgentApplication { isApplicationInstalled: boolean = false; termsAndConditionsAccepted: boolean = false; agentName = 'A365 Agent'; + authHandlerName = 'agentic'; constructor() { super(); @@ -61,7 +62,7 @@ export class A365Agent extends AgentApplication { } 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) { diff --git a/tests-agent/openai-agent-auto-instrument-sample/src/OpenAIClient.ts b/tests-agent/openai-agent-auto-instrument-sample/src/OpenAIClient.ts index 0179be32..96cf4e6e 100644 --- a/tests-agent/openai-agent-auto-instrument-sample/src/OpenAIClient.ts +++ b/tests-agent/openai-agent-auto-instrument-sample/src/OpenAIClient.ts @@ -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 { +export async function getClient(authorization: Authorization | undefined, authHandlerName: string, turnContext: TurnContext): Promise { const agent = new Agent({ // You can customize the agent configuration here if needed name: 'OpenAI Agent', @@ -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 || '', );