From c3291f38ad01f29e86ded6e1f97f9478b8affb1e Mon Sep 17 00:00:00 2001 From: Rick Brighenti <202984599+rbrighenti@users.noreply.github.com> Date: Thu, 20 Nov 2025 15:50:53 +0000 Subject: [PATCH] Fix agentic user token retrieval for n8n Sample --- nodejs/n8n/sample-agent/.env.template | 15 +++--- .../sample-agent/Agent-Code-Walkthrough.MD | 17 +------ nodejs/n8n/sample-agent/package.json | 13 ++--- nodejs/n8n/sample-agent/src/agent.ts | 31 +++++------- nodejs/n8n/sample-agent/src/index.ts | 8 ++-- .../src/mcpToolRegistrationService.ts | 11 +++-- nodejs/n8n/sample-agent/src/n8nAgent.ts | 48 ++++--------------- nodejs/n8n/sample-agent/src/n8nClient.ts | 3 ++ nodejs/n8n/sample-agent/src/telemetry.ts | 3 ++ 9 files changed, 53 insertions(+), 96 deletions(-) diff --git a/nodejs/n8n/sample-agent/.env.template b/nodejs/n8n/sample-agent/.env.template index 788edb8f..6f65ea39 100644 --- a/nodejs/n8n/sample-agent/.env.template +++ b/nodejs/n8n/sample-agent/.env.template @@ -16,17 +16,14 @@ PORT=3978 MCP_AUTH_TOKEN= TOOLS_MODE=MCPPlatform -# Service Connection Settings -connections__service_connection__settings__clientId= -connections__service_connection__settings__clientSecret= -connections__service_connection__settings__tenantId= +#Auth +connections__service_connection__settings__clientId=blueprint_id +connections__service_connection__settings__clientSecret=blueprint_secret +connections__service_connection__settings__tenantId=tenant_id -# Set service connection as default -connectionsMap__0__serviceUrl=* connectionsMap__0__connection=service_connection +connectionsMap__0__serviceUrl=* -# AgenticAuthentication Options agentic_type=agentic -agentic_altBlueprintConnectionName=service_connection -agentic_scopes=ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default # Prod Agentic scope +agentic_scopes=https://graph.microsoft.com/.default agentic_connectionName=service_connection \ No newline at end of file diff --git a/nodejs/n8n/sample-agent/Agent-Code-Walkthrough.MD b/nodejs/n8n/sample-agent/Agent-Code-Walkthrough.MD index ffe8c200..fcbf0699 100644 --- a/nodejs/n8n/sample-agent/Agent-Code-Walkthrough.MD +++ b/nodejs/n8n/sample-agent/Agent-Code-Walkthrough.MD @@ -110,14 +110,12 @@ TOOLS_MODE=MCPPlatform - Handles graceful shutdown on SIGINT/SIGTERM signals #### agent.ts - Agent Application Setup -- Defines conversation state interface (`ConversationState` with message count) - Creates `AgentApplication` with memory storage and file download support - Registers activity handlers: - - **Message Activity**: Increments counter, delegates to `N8nAgent.handleAgentMessageActivity` + - **Message Activity**: Delegates to `N8nAgent.handleAgentMessageActivity` - **InstallationUpdate Activity**: Delegates to `N8nAgent.handleInstallationUpdateActivity` #### n8nAgent.ts - Core Business Logic -- Manages agent state: `isApplicationInstalled` and `termsAndConditionsAccepted` - **Message Handler**: Enforces installation → terms acceptance → processing flow - **Installation Handler**: Sets state flags and sends welcome/goodbye messages - **Notification Handlers**: Processes email and Word @-mention notifications with two-stage flow (metadata extraction → content retrieval via n8n) @@ -153,19 +151,6 @@ TOOLS_MODE=MCPPlatform - **mcpToolRegistrationService.ts**: Tool management - **telemetry.ts**: Observability configuration -### Lifecycle State Management -```typescript -// Installation → Terms → Active → Uninstall -isApplicationInstalled: false → true → true → false -termsAndConditionsAccepted: false → false → true → false -``` - -**Flow**: -1. User installs agent → Welcome message -2. User sends "I accept" → Terms accepted -3. User sends messages → Forwarded to n8n -4. Answer is sent back to user - ### Observability-First Design - All n8n invocations wrapped with InvokeAgentScope - Inference operations tracked with InferenceScope diff --git a/nodejs/n8n/sample-agent/package.json b/nodejs/n8n/sample-agent/package.json index ac233be8..784bc7d1 100644 --- a/nodejs/n8n/sample-agent/package.json +++ b/nodejs/n8n/sample-agent/package.json @@ -13,19 +13,20 @@ "author": "", "license": "ISC", "dependencies": { + "@microsoft/agents-hosting": "^1.1.0-alpha.85", "@microsoft/agents-hosting-express": "^1.1.0-alpha.85", "@microsoft/agents-a365-notifications": "*", "@microsoft/agents-a365-observability": "*", "@microsoft/agents-a365-runtime": "*", "@microsoft/agents-a365-tooling": "*", - "@modelcontextprotocol/sdk": "^1.18.1", + "@modelcontextprotocol/sdk": "^1.22.0", "express": "^5.1.0", - "dotenv": "^17.2.2" + "dotenv": "^17.2.3" }, "devDependencies": { - "tsx": "^4.20.5", - "@microsoft/m365agentsplayground": "^0.2.18", - "@types/node": "^24.5.1", - "typescript": "^5.0.0" + "tsx": "^4.20.6", + "@microsoft/m365agentsplayground": "^0.2.20", + "@types/node": "^24.10.1", + "typescript": "^5.9.3" } } diff --git a/nodejs/n8n/sample-agent/src/agent.ts b/nodejs/n8n/sample-agent/src/agent.ts index c77adcb2..559fe6bd 100644 --- a/nodejs/n8n/sample-agent/src/agent.ts +++ b/nodejs/n8n/sample-agent/src/agent.ts @@ -1,31 +1,24 @@ -import { TurnState, AgentApplication, AttachmentDownloader, MemoryStorage, TurnContext } from '@microsoft/agents-hosting'; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { TurnState, AgentApplicationBuilder, MemoryStorage, TurnContext } from '@microsoft/agents-hosting'; import { ActivityTypes } from '@microsoft/agents-activity'; import { N8nAgent } from './n8nAgent'; -interface ConversationState { - count: number; -} -type ApplicationTurnState = TurnState - -const downloader = new AttachmentDownloader(); const storage = new MemoryStorage(); -export const agentApplication = new AgentApplication({ - storage, - fileDownloaders: [downloader] -}); +export const agentApplication = + new AgentApplicationBuilder() + .withAuthorization({ agentic: {} }) + .withStorage(storage) + .build(); -const n8nAgent = new N8nAgent(); - -agentApplication.onActivity(ActivityTypes.Message, async (context: TurnContext, state: ApplicationTurnState) => { - // Increment count state - let count = state.conversation.count ?? 0; - state.conversation.count = ++count; +const n8nAgent = new N8nAgent(agentApplication); +agentApplication.onActivity(ActivityTypes.Message, async (context: TurnContext, state: TurnState) => { await n8nAgent.handleAgentMessageActivity(context, state); }); -agentApplication.onActivity(ActivityTypes.InstallationUpdate, async (context: TurnContext, state: ApplicationTurnState) => { +agentApplication.onActivity(ActivityTypes.InstallationUpdate, async (context: TurnContext, state: TurnState) => { await n8nAgent.handleInstallationUpdateActivity(context, state); }); - diff --git a/nodejs/n8n/sample-agent/src/index.ts b/nodejs/n8n/sample-agent/src/index.ts index d9f1c83e..580efede 100644 --- a/nodejs/n8n/sample-agent/src/index.ts +++ b/nodejs/n8n/sample-agent/src/index.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + import express, { Response } from 'express'; import 'dotenv/config'; import { AuthConfiguration, authorizeJWT, CloudAdapter, loadAuthConfigFromEnv, Request } from '@microsoft/agents-hosting'; @@ -5,7 +8,7 @@ import { observabilityManager } from './telemetry'; import { agentApplication } from './agent'; const authConfig: AuthConfiguration = loadAuthConfigFromEnv(); -const adapter = new CloudAdapter(authConfig); +const adapter = agentApplication.adapter as CloudAdapter; const app = express(); const port = process.env.PORT ?? 3978; @@ -23,7 +26,7 @@ app.post('/api/messages', async (req: Request, res: Response) => { }); const server = app.listen(port, () => { - console.log(`\nServer listening to port ${port} for appId ${authConfig.clientId} debug ${process.env.DEBUG}`); + console.log(`\nServer listening to port ${port} for appId ${authConfig.clientId} debug ${!!process.env.DEBUG}`); }).on('error', async (err) => { console.error(err); await observabilityManager.shutdown(); @@ -41,7 +44,6 @@ process.on('SIGINT', () => { }); }); - process.on('SIGTERM', () => { server.close(() => { console.log('Server closed.'); diff --git a/nodejs/n8n/sample-agent/src/mcpToolRegistrationService.ts b/nodejs/n8n/sample-agent/src/mcpToolRegistrationService.ts index e5bd435b..85bf7df4 100644 --- a/nodejs/n8n/sample-agent/src/mcpToolRegistrationService.ts +++ b/nodejs/n8n/sample-agent/src/mcpToolRegistrationService.ts @@ -1,8 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + import { McpToolServerConfigurationService, McpClientTool, MCPServerConfig } from '@microsoft/agents-a365-tooling'; -import { AgenticAuthenticationService, Authorization, Utility as RuntimeUtility } from '@microsoft/agents-a365-runtime'; +import { AgenticAuthenticationService, Utility as RuntimeUtility } from '@microsoft/agents-a365-runtime'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import { TurnContext } from '@microsoft/agents-hosting'; +import { AgentApplication, TurnContext, TurnState } from '@microsoft/agents-hosting'; export type McpServer = MCPServerConfig & { type: string, @@ -22,12 +25,14 @@ export class McpToolRegistrationService { async getMcpServers( authHandlerName: string, turnContext: TurnContext, + agentApplication: AgentApplication, authToken: string ): Promise { - const authorization = turnContext.turnState.get('authorization'); if (!authToken) { + const authorization = agentApplication.authorization; authToken = await AgenticAuthenticationService.GetAgenticUserToken(authorization, authHandlerName, turnContext); } + // Get the agentic user ID from authorization configuration const agenticAppId = RuntimeUtility.ResolveAgentIdentity(turnContext, authToken); diff --git a/nodejs/n8n/sample-agent/src/n8nAgent.ts b/nodejs/n8n/sample-agent/src/n8nAgent.ts index 8a73aac5..a1cca096 100644 --- a/nodejs/n8n/sample-agent/src/n8nAgent.ts +++ b/nodejs/n8n/sample-agent/src/n8nAgent.ts @@ -1,37 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + import { AgentNotificationActivity, NotificationType, createAgentNotificationActivity } from '@microsoft/agents-a365-notifications'; -import { TurnContext, TurnState } from '@microsoft/agents-hosting'; +import { AgentApplication, TurnContext, TurnState } from '@microsoft/agents-hosting'; import { N8nClient } from './n8nClient'; import { McpToolRegistrationService, McpServer } from './mcpToolRegistrationService'; export class N8nAgent { static authHandlerName: string = 'agentic'; - isApplicationInstalled: boolean = false; - termsAndConditionsAccepted: boolean = false; toolService: McpToolRegistrationService = new McpToolRegistrationService(); + agentApplication: AgentApplication; - constructor() { + constructor(agentApplication: AgentApplication) { + this.agentApplication = agentApplication; } /** * Handles incoming user messages and sends responses using n8n. */ async handleAgentMessageActivity(turnContext: TurnContext, state: TurnState): Promise { - if (!this.isApplicationInstalled) { - await turnContext.sendActivity("Please install the application before sending messages."); - return; - } - - if (!this.termsAndConditionsAccepted) { - if (turnContext.activity.text?.trim().toLowerCase() === "i accept") { - this.termsAndConditionsAccepted = true; - await turnContext.sendActivity("Thank you for accepting the terms and conditions! How can I assist you today?"); - return; - } else { - await turnContext.sendActivity("Please accept the terms and conditions to proceed. Send 'I accept' to accept."); - return; - } - } - const userMessage = turnContext.activity.text?.trim() || ''; const fromUser = turnContext.activity.from?.name || ''; @@ -62,22 +49,6 @@ export class N8nAgent { return; } - if (!this.isApplicationInstalled) { - await turnContext.sendActivity("Please install the application before sending notifications."); - return; - } - - if (!this.termsAndConditionsAccepted) { - if (turnContext.activity.text?.trim().toLowerCase() === "i accept") { - this.termsAndConditionsAccepted = true; - await turnContext.sendActivity("Thank you for accepting the terms and conditions! How can I assist you today?"); - return; - } else { - await turnContext.sendActivity("Please accept the terms and conditions to proceed. Send 'I accept' to accept."); - return; - } - } - // Find the first known notification type entity const agentNotificationActivity = createAgentNotificationActivity(activity); @@ -103,12 +74,8 @@ export class N8nAgent { */ async handleInstallationUpdateActivity(turnContext: TurnContext, state: TurnState): Promise { if (turnContext.activity.action === 'add') { - this.isApplicationInstalled = true; - this.termsAndConditionsAccepted = false; await turnContext.sendActivity('Thank you for hiring me! Looking forward to assisting you in your professional journey! Before I begin, could you please confirm that you accept the terms and conditions? Send "I accept" to accept.'); } else if (turnContext.activity.action === 'remove') { - this.isApplicationInstalled = false; - this.termsAndConditionsAccepted = false; await turnContext.sendActivity('Thank you for your time, I enjoyed working with you.'); } } @@ -182,6 +149,7 @@ export class N8nAgent { mcpServers.push(...await this.toolService.getMcpServers( N8nAgent.authHandlerName, turnContext, + this.agentApplication, process.env.MCP_AUTH_TOKEN || "" )); } catch (error) { diff --git a/nodejs/n8n/sample-agent/src/n8nClient.ts b/nodejs/n8n/sample-agent/src/n8nClient.ts index 41546df0..9a5931f8 100644 --- a/nodejs/n8n/sample-agent/src/n8nClient.ts +++ b/nodejs/n8n/sample-agent/src/n8nClient.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + import { InferenceScope, InvokeAgentScope, TenantDetails, InvokeAgentDetails, InferenceOperationType } from '@microsoft/agents-a365-observability'; import { McpServer } from './mcpToolRegistrationService'; diff --git a/nodejs/n8n/sample-agent/src/telemetry.ts b/nodejs/n8n/sample-agent/src/telemetry.ts index 558708f1..6c6bf310 100644 --- a/nodejs/n8n/sample-agent/src/telemetry.ts +++ b/nodejs/n8n/sample-agent/src/telemetry.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + import { ObservabilityManager, } from '@microsoft/agents-a365-observability';