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
15 changes: 6 additions & 9 deletions nodejs/n8n/sample-agent/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 1 addition & 16 deletions nodejs/n8n/sample-agent/Agent-Code-Walkthrough.MD
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
13 changes: 7 additions & 6 deletions nodejs/n8n/sample-agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
31 changes: 12 additions & 19 deletions nodejs/n8n/sample-agent/src/agent.ts
Original file line number Diff line number Diff line change
@@ -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<ConversationState>

const downloader = new AttachmentDownloader();
const storage = new MemoryStorage();

export const agentApplication = new AgentApplication<ApplicationTurnState>({
storage,
fileDownloaders: [downloader]
});
export const agentApplication =
new AgentApplicationBuilder<TurnState>()
.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);
});

8 changes: 5 additions & 3 deletions nodejs/n8n/sample-agent/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
// 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';
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;

Expand All @@ -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();
Expand All @@ -41,7 +44,6 @@ process.on('SIGINT', () => {
});
});


process.on('SIGTERM', () => {
server.close(() => {
console.log('Server closed.');
Expand Down
11 changes: 8 additions & 3 deletions nodejs/n8n/sample-agent/src/mcpToolRegistrationService.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -22,12 +25,14 @@ export class McpToolRegistrationService {
async getMcpServers(
authHandlerName: string,
turnContext: TurnContext,
agentApplication: AgentApplication<TurnState>,
authToken: string
): Promise<McpServer[]> {
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);

Expand Down
48 changes: 8 additions & 40 deletions nodejs/n8n/sample-agent/src/n8nAgent.ts
Original file line number Diff line number Diff line change
@@ -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<TurnState>;

constructor() {
constructor(agentApplication: AgentApplication<TurnState>) {
this.agentApplication = agentApplication;
}

/**
* Handles incoming user messages and sends responses using n8n.
*/
async handleAgentMessageActivity(turnContext: TurnContext, state: TurnState): Promise<void> {
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 || '';

Expand Down Expand Up @@ -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);

Expand All @@ -103,12 +74,8 @@ export class N8nAgent {
*/
async handleInstallationUpdateActivity(turnContext: TurnContext, state: TurnState): Promise<void> {
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.');
}
}
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions nodejs/n8n/sample-agent/src/n8nClient.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
3 changes: 3 additions & 0 deletions nodejs/n8n/sample-agent/src/telemetry.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import {
ObservabilityManager,
} from '@microsoft/agents-a365-observability';
Expand Down