From 74ded7b5dfbb2881a9da185ad11d34ce41d32a6c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Dec 2025 18:31:07 +0000 Subject: [PATCH 01/14] Initial plan From 092abade57484b599c17c7312aca64cbb200fed7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Dec 2025 18:40:21 +0000 Subject: [PATCH 02/14] Add support for custom MCP server URLs in ToolingManifest.json Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> --- .../src/McpToolServerConfigurationService.ts | 9 +- .../McpToolServerConfigurationService.test.ts | 158 ++++++++++++++++++ 2 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 tests/tooling/McpToolServerConfigurationService.test.ts diff --git a/packages/agents-a365-tooling/src/McpToolServerConfigurationService.ts b/packages/agents-a365-tooling/src/McpToolServerConfigurationService.ts index 3fd16f9f..2803416c 100644 --- a/packages/agents-a365-tooling/src/McpToolServerConfigurationService.ts +++ b/packages/agents-a365-tooling/src/McpToolServerConfigurationService.ts @@ -129,9 +129,16 @@ export class McpToolServerConfigurationService { * { * "mcpServerName": "sharePointMCPServerConfig", * "mcpServerUniqueName": "mcp_SharePointTools" + * }, + * { + * "mcpServerName": "customMCPServer", + * "url": "http://localhost:3000/mcp" * } * ] * } + * + * Each server entry can optionally include a "url" field to specify a custom MCP server URL. + * If the "url" field is not provided, the URL will be automatically constructed using the server name. */ private async getMCPServerConfigsFromManifest(): Promise { let manifestPath = path.join(process.cwd(), 'ToolingManifest.json'); @@ -153,7 +160,7 @@ export class McpToolServerConfigurationService { return mcpServers.map((s: MCPServerConfig) => { return { mcpServerName: s.mcpServerName, - url: Utility.BuildMcpServerUrl(s.mcpServerName) + url: s.url || Utility.BuildMcpServerUrl(s.mcpServerName) }; }); } catch (err: unknown) { diff --git a/tests/tooling/McpToolServerConfigurationService.test.ts b/tests/tooling/McpToolServerConfigurationService.test.ts new file mode 100644 index 00000000..aee17f02 --- /dev/null +++ b/tests/tooling/McpToolServerConfigurationService.test.ts @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals'; +import { McpToolServerConfigurationService } from '../../packages/agents-a365-tooling/src/McpToolServerConfigurationService'; +import fs from 'fs'; +import path from 'path'; + +describe('McpToolServerConfigurationService', () => { + let service: McpToolServerConfigurationService; + let originalEnv: NodeJS.ProcessEnv; + + beforeEach(() => { + service = new McpToolServerConfigurationService(); + originalEnv = { ...process.env }; + // Set to development mode to read from manifest + process.env.NODE_ENV = 'Development'; + }); + + afterEach(() => { + process.env = originalEnv; + jest.restoreAllMocks(); + }); + + describe('listToolServers with custom URLs', () => { + it('should use custom URL when provided in manifest', async () => { + // Arrange + const mockManifestPath = path.join(process.cwd(), 'ToolingManifest.json'); + const manifestContent = { + mcpServers: [ + { + mcpServerName: 'customServer', + url: 'http://localhost:3000/custom-mcp' + } + ] + }; + + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(manifestContent)); + + // Act + const servers = await service.listToolServers('test-agent-id', 'mock-auth-token'); + + // Assert + expect(servers).toHaveLength(1); + expect(servers[0].mcpServerName).toBe('customServer'); + expect(servers[0].url).toBe('http://localhost:3000/custom-mcp'); + }); + + it('should build URL when not provided in manifest', async () => { + // Arrange + const mockManifestPath = path.join(process.cwd(), 'ToolingManifest.json'); + const manifestContent = { + mcpServers: [ + { + mcpServerName: 'mcp_MailTools' + } + ] + }; + + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(manifestContent)); + + // Act + const servers = await service.listToolServers('test-agent-id', 'mock-auth-token'); + + // Assert + expect(servers).toHaveLength(1); + expect(servers[0].mcpServerName).toBe('mcp_MailTools'); + // In development mode, should use mock server URL + expect(servers[0].url).toContain('mcp_MailTools'); + }); + + it('should handle mix of custom and default URLs in manifest', async () => { + // Arrange + const mockManifestPath = path.join(process.cwd(), 'ToolingManifest.json'); + const manifestContent = { + mcpServers: [ + { + mcpServerName: 'customServer', + url: 'https://custom.example.com/mcp' + }, + { + mcpServerName: 'mcp_MailTools' + }, + { + mcpServerName: 'anotherCustom', + url: 'http://localhost:5000/mcp-server' + } + ] + }; + + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(manifestContent)); + + // Act + const servers = await service.listToolServers('test-agent-id', 'mock-auth-token'); + + // Assert + expect(servers).toHaveLength(3); + + // First server has custom URL + expect(servers[0].mcpServerName).toBe('customServer'); + expect(servers[0].url).toBe('https://custom.example.com/mcp'); + + // Second server uses default URL building + expect(servers[1].mcpServerName).toBe('mcp_MailTools'); + expect(servers[1].url).toContain('mcp_MailTools'); + + // Third server has custom URL + expect(servers[2].mcpServerName).toBe('anotherCustom'); + expect(servers[2].url).toBe('http://localhost:5000/mcp-server'); + }); + + it('should return empty array when manifest file does not exist', async () => { + // Arrange + jest.spyOn(fs, 'existsSync').mockReturnValue(false); + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + // Act + const servers = await service.listToolServers('test-agent-id', 'mock-auth-token'); + + // Assert + expect(servers).toHaveLength(0); + expect(consoleWarnSpy).toHaveBeenCalled(); + }); + + it('should handle empty mcpServers array in manifest', async () => { + // Arrange + const manifestContent = { + mcpServers: [] + }; + + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(manifestContent)); + + // Act + const servers = await service.listToolServers('test-agent-id', 'mock-auth-token'); + + // Assert + expect(servers).toHaveLength(0); + }); + + it('should handle missing mcpServers property in manifest', async () => { + // Arrange + const manifestContent = {}; + + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(manifestContent)); + + // Act + const servers = await service.listToolServers('test-agent-id', 'mock-auth-token'); + + // Assert + expect(servers).toHaveLength(0); + }); + }); +}); From 2b89568ae5ad38d2dad288cd8a08e76d1e905568 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Dec 2025 18:44:01 +0000 Subject: [PATCH 03/14] Address code review feedback: cleanup unused variables and improve type safety Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> --- packages/agents-a365-tooling/src/contracts.ts | 2 +- tests/tooling/McpToolServerConfigurationService.test.ts | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/agents-a365-tooling/src/contracts.ts b/packages/agents-a365-tooling/src/contracts.ts index 79831a53..891fa9d2 100644 --- a/packages/agents-a365-tooling/src/contracts.ts +++ b/packages/agents-a365-tooling/src/contracts.ts @@ -3,7 +3,7 @@ export interface MCPServerConfig { mcpServerName: string; - url: string; + url?: string; headers?: Record; } diff --git a/tests/tooling/McpToolServerConfigurationService.test.ts b/tests/tooling/McpToolServerConfigurationService.test.ts index aee17f02..27b5ad49 100644 --- a/tests/tooling/McpToolServerConfigurationService.test.ts +++ b/tests/tooling/McpToolServerConfigurationService.test.ts @@ -4,7 +4,6 @@ import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals'; import { McpToolServerConfigurationService } from '../../packages/agents-a365-tooling/src/McpToolServerConfigurationService'; import fs from 'fs'; -import path from 'path'; describe('McpToolServerConfigurationService', () => { let service: McpToolServerConfigurationService; @@ -25,7 +24,6 @@ describe('McpToolServerConfigurationService', () => { describe('listToolServers with custom URLs', () => { it('should use custom URL when provided in manifest', async () => { // Arrange - const mockManifestPath = path.join(process.cwd(), 'ToolingManifest.json'); const manifestContent = { mcpServers: [ { @@ -49,7 +47,6 @@ describe('McpToolServerConfigurationService', () => { it('should build URL when not provided in manifest', async () => { // Arrange - const mockManifestPath = path.join(process.cwd(), 'ToolingManifest.json'); const manifestContent = { mcpServers: [ { @@ -73,7 +70,6 @@ describe('McpToolServerConfigurationService', () => { it('should handle mix of custom and default URLs in manifest', async () => { // Arrange - const mockManifestPath = path.join(process.cwd(), 'ToolingManifest.json'); const manifestContent = { mcpServers: [ { From c00b41a46cd2396778646ec1d5ed5937af928e35 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Dec 2025 20:38:06 +0000 Subject: [PATCH 04/14] Revert url field to required in MCPServerConfig interface Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> --- packages/agents-a365-tooling/src/contracts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/agents-a365-tooling/src/contracts.ts b/packages/agents-a365-tooling/src/contracts.ts index 891fa9d2..79831a53 100644 --- a/packages/agents-a365-tooling/src/contracts.ts +++ b/packages/agents-a365-tooling/src/contracts.ts @@ -3,7 +3,7 @@ export interface MCPServerConfig { mcpServerName: string; - url?: string; + url: string; headers?: Record; } From 4d75806c0fe09080a097ce9d761418ac6f7ef0b0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Dec 2025 21:08:09 +0000 Subject: [PATCH 05/14] Validate full URL in tests using Utility.BuildMcpServerUrl Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> --- tests/tooling/McpToolServerConfigurationService.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/tooling/McpToolServerConfigurationService.test.ts b/tests/tooling/McpToolServerConfigurationService.test.ts index 27b5ad49..0e12a26a 100644 --- a/tests/tooling/McpToolServerConfigurationService.test.ts +++ b/tests/tooling/McpToolServerConfigurationService.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals'; import { McpToolServerConfigurationService } from '../../packages/agents-a365-tooling/src/McpToolServerConfigurationService'; +import { Utility } from '../../packages/agents-a365-tooling/src/Utility'; import fs from 'fs'; describe('McpToolServerConfigurationService', () => { @@ -65,7 +66,7 @@ describe('McpToolServerConfigurationService', () => { expect(servers).toHaveLength(1); expect(servers[0].mcpServerName).toBe('mcp_MailTools'); // In development mode, should use mock server URL - expect(servers[0].url).toContain('mcp_MailTools'); + expect(servers[0].url).toBe(Utility.BuildMcpServerUrl('mcp_MailTools')); }); it('should handle mix of custom and default URLs in manifest', async () => { @@ -101,7 +102,7 @@ describe('McpToolServerConfigurationService', () => { // Second server uses default URL building expect(servers[1].mcpServerName).toBe('mcp_MailTools'); - expect(servers[1].url).toContain('mcp_MailTools'); + expect(servers[1].url).toBe(Utility.BuildMcpServerUrl('mcp_MailTools')); // Third server has custom URL expect(servers[2].mcpServerName).toBe('anotherCustom'); From bd15415d71cd9c7eff4abbcab8c2cba576fb28aa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Dec 2025 21:14:00 +0000 Subject: [PATCH 06/14] Use mcpServerUniqueName as fallback when mcpServerName is not set Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> --- .../src/McpToolServerConfigurationService.ts | 9 ++-- .../McpToolServerConfigurationService.test.ts | 45 +++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/packages/agents-a365-tooling/src/McpToolServerConfigurationService.ts b/packages/agents-a365-tooling/src/McpToolServerConfigurationService.ts index 2803416c..d5f155a7 100644 --- a/packages/agents-a365-tooling/src/McpToolServerConfigurationService.ts +++ b/packages/agents-a365-tooling/src/McpToolServerConfigurationService.ts @@ -139,6 +139,7 @@ export class McpToolServerConfigurationService { * * Each server entry can optionally include a "url" field to specify a custom MCP server URL. * If the "url" field is not provided, the URL will be automatically constructed using the server name. + * The server name is determined by using "mcpServerName" if present, otherwise "mcpServerUniqueName". */ private async getMCPServerConfigsFromManifest(): Promise { let manifestPath = path.join(process.cwd(), 'ToolingManifest.json'); @@ -157,10 +158,12 @@ export class McpToolServerConfigurationService { const manifestData = JSON.parse(jsonContent); const mcpServers = manifestData.mcpServers || []; - return mcpServers.map((s: MCPServerConfig) => { + return mcpServers.map((s: any) => { + // Use mcpServerName if available, otherwise fall back to mcpServerUniqueName + const serverName = s.mcpServerName || s.mcpServerUniqueName; return { - mcpServerName: s.mcpServerName, - url: s.url || Utility.BuildMcpServerUrl(s.mcpServerName) + mcpServerName: serverName, + url: s.url || Utility.BuildMcpServerUrl(serverName) }; }); } catch (err: unknown) { diff --git a/tests/tooling/McpToolServerConfigurationService.test.ts b/tests/tooling/McpToolServerConfigurationService.test.ts index 0e12a26a..31591631 100644 --- a/tests/tooling/McpToolServerConfigurationService.test.ts +++ b/tests/tooling/McpToolServerConfigurationService.test.ts @@ -151,5 +151,50 @@ describe('McpToolServerConfigurationService', () => { // Assert expect(servers).toHaveLength(0); }); + + it('should use mcpServerUniqueName as fallback when mcpServerName is not provided', async () => { + // Arrange + const manifestContent = { + mcpServers: [ + { + mcpServerUniqueName: 'mcp_UniqueServer' + } + ] + }; + + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(manifestContent)); + + // Act + const servers = await service.listToolServers('test-agent-id', 'mock-auth-token'); + + // Assert + expect(servers).toHaveLength(1); + expect(servers[0].mcpServerName).toBe('mcp_UniqueServer'); + expect(servers[0].url).toBe(Utility.BuildMcpServerUrl('mcp_UniqueServer')); + }); + + it('should prefer mcpServerName over mcpServerUniqueName when both are provided', async () => { + // Arrange + const manifestContent = { + mcpServers: [ + { + mcpServerName: 'mcp_PreferredName', + mcpServerUniqueName: 'mcp_FallbackName' + } + ] + }; + + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(manifestContent)); + + // Act + const servers = await service.listToolServers('test-agent-id', 'mock-auth-token'); + + // Assert + expect(servers).toHaveLength(1); + expect(servers[0].mcpServerName).toBe('mcp_PreferredName'); + expect(servers[0].url).toBe(Utility.BuildMcpServerUrl('mcp_PreferredName')); + }); }); }); From 98f17bb84fe61ca5bae786b8b49d0a8314582820 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Dec 2025 16:21:58 +0000 Subject: [PATCH 07/14] Replace 'any' type with proper MCPServerManifestEntry interface Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> --- .../src/McpToolServerConfigurationService.ts | 7 ++++-- packages/agents-a365-tooling/src/contracts.ts | 7 ++++++ .../McpToolServerConfigurationService.test.ts | 24 +++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/agents-a365-tooling/src/McpToolServerConfigurationService.ts b/packages/agents-a365-tooling/src/McpToolServerConfigurationService.ts index d5f155a7..e281a8cd 100644 --- a/packages/agents-a365-tooling/src/McpToolServerConfigurationService.ts +++ b/packages/agents-a365-tooling/src/McpToolServerConfigurationService.ts @@ -4,7 +4,7 @@ import fs from 'fs'; import path from 'path'; import axios from 'axios'; -import { MCPServerConfig, McpClientTool, ToolOptions } from './contracts'; +import { MCPServerConfig, MCPServerManifestEntry, McpClientTool, ToolOptions } from './contracts'; import { Utility } from './Utility'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; @@ -158,9 +158,12 @@ export class McpToolServerConfigurationService { const manifestData = JSON.parse(jsonContent); const mcpServers = manifestData.mcpServers || []; - return mcpServers.map((s: any) => { + return mcpServers.map((s: MCPServerManifestEntry) => { // Use mcpServerName if available, otherwise fall back to mcpServerUniqueName const serverName = s.mcpServerName || s.mcpServerUniqueName; + if (!serverName) { + throw new Error('Either mcpServerName or mcpServerUniqueName must be provided in manifest entry'); + } return { mcpServerName: serverName, url: s.url || Utility.BuildMcpServerUrl(serverName) diff --git a/packages/agents-a365-tooling/src/contracts.ts b/packages/agents-a365-tooling/src/contracts.ts index 79831a53..fe188893 100644 --- a/packages/agents-a365-tooling/src/contracts.ts +++ b/packages/agents-a365-tooling/src/contracts.ts @@ -7,6 +7,13 @@ export interface MCPServerConfig { headers?: Record; } +export interface MCPServerManifestEntry { + mcpServerName?: string; + mcpServerUniqueName?: string; + url?: string; + headers?: Record; +} + export interface McpClientTool { name: string; description?: string; diff --git a/tests/tooling/McpToolServerConfigurationService.test.ts b/tests/tooling/McpToolServerConfigurationService.test.ts index 31591631..422ecd7e 100644 --- a/tests/tooling/McpToolServerConfigurationService.test.ts +++ b/tests/tooling/McpToolServerConfigurationService.test.ts @@ -196,5 +196,29 @@ describe('McpToolServerConfigurationService', () => { expect(servers[0].mcpServerName).toBe('mcp_PreferredName'); expect(servers[0].url).toBe(Utility.BuildMcpServerUrl('mcp_PreferredName')); }); + + it('should return empty array and log error when neither mcpServerName nor mcpServerUniqueName is provided', async () => { + // Arrange + const manifestContent = { + mcpServers: [ + { + url: 'http://localhost:3000/custom-mcp' + } + ] + }; + + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(manifestContent)); + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + // Act + const servers = await service.listToolServers('test-agent-id', 'mock-auth-token'); + + // Assert + expect(servers).toHaveLength(0); + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining('Either mcpServerName or mcpServerUniqueName must be provided') + ); + }); }); }); From d8ca535af8004c7cf8de9a500825b297badfd749 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Dec 2025 16:42:44 +0000 Subject: [PATCH 08/14] Fix test environment variable handling to match existing pattern Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> --- tests/tooling/McpToolServerConfigurationService.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/tooling/McpToolServerConfigurationService.test.ts b/tests/tooling/McpToolServerConfigurationService.test.ts index 422ecd7e..ce139778 100644 --- a/tests/tooling/McpToolServerConfigurationService.test.ts +++ b/tests/tooling/McpToolServerConfigurationService.test.ts @@ -8,11 +8,11 @@ import fs from 'fs'; describe('McpToolServerConfigurationService', () => { let service: McpToolServerConfigurationService; - let originalEnv: NodeJS.ProcessEnv; + const originalEnv = process.env; beforeEach(() => { service = new McpToolServerConfigurationService(); - originalEnv = { ...process.env }; + process.env = { ...originalEnv }; // Set to development mode to read from manifest process.env.NODE_ENV = 'Development'; }); From 4c37d49daeb86988404d2afae0e10c65b5fa0f68 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Dec 2025 17:02:38 +0000 Subject: [PATCH 09/14] Add --experimental-vm-modules flag to test scripts for ESM support Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> --- tests/package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/package.json b/tests/package.json index 90f88709..a5938964 100644 --- a/tests/package.json +++ b/tests/package.json @@ -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_OPTIONS='--experimental-vm-modules' jest --config jest.config.cjs --passWithNoTests --testPathIgnorePatterns=/integration/", + "test:watch": "NODE_OPTIONS='--experimental-vm-modules' jest --config jest.config.cjs --watch --testPathIgnorePatterns=/integration/", + "test:coverage": "NODE_OPTIONS='--experimental-vm-modules' jest --config jest.config.cjs --coverage --passWithNoTests --testPathIgnorePatterns=/integration/", + "test:verbose": "NODE_OPTIONS='--experimental-vm-modules' jest --config jest.config.cjs --verbose --passWithNoTests --testPathIgnorePatterns=/integration/", + "test:ci": "NODE_OPTIONS='--experimental-vm-modules' jest --config jest.config.cjs --coverage --ci --maxWorkers=2 --passWithNoTests --testPathIgnorePatterns=/integration/" }, "keywords": [ "agents", From de9a135377ec72bf29bad5680b10dbb3e8ed699d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Dec 2025 17:16:18 +0000 Subject: [PATCH 10/14] Use cross-env for cross-platform NODE_OPTIONS compatibility Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> --- pnpm-lock.yaml | 15 +++++++++++++++ pnpm-workspace.yaml | 1 + tests/package.json | 11 ++++++----- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3bb19b9e..9b8bbb76 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,6 +87,9 @@ catalogs: '@typescript-eslint/parser': specifier: ^8.47.0 version: 8.47.0 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 dotenv: specifier: ^17.2.2 version: 17.2.3 @@ -699,6 +702,9 @@ importers: '@typescript-eslint/parser': specifier: 'catalog:' version: 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + cross-env: + specifier: 'catalog:' + version: 7.0.3 eslint: specifier: 'catalog:' version: 9.39.1(jiti@2.6.1) @@ -2322,6 +2328,11 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -5807,6 +5818,10 @@ snapshots: create-require@1.1.1: {} + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.6 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a78ab54c..ed0ad2ac 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -56,6 +56,7 @@ catalog: "@types/node": "^20.17.0" "@typescript-eslint/eslint-plugin": "^8.47.0" "@typescript-eslint/parser": "^8.47.0" + "cross-env": "^7.0.3" "dotenv": "^17.2.2" "eslint": "^9.39.1" "jest": "^30.2.0" diff --git a/tests/package.json b/tests/package.json index a5938964..065306e0 100644 --- a/tests/package.json +++ b/tests/package.json @@ -6,11 +6,11 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { - "test": "NODE_OPTIONS='--experimental-vm-modules' jest --config jest.config.cjs --passWithNoTests --testPathIgnorePatterns=/integration/", - "test:watch": "NODE_OPTIONS='--experimental-vm-modules' jest --config jest.config.cjs --watch --testPathIgnorePatterns=/integration/", - "test:coverage": "NODE_OPTIONS='--experimental-vm-modules' jest --config jest.config.cjs --coverage --passWithNoTests --testPathIgnorePatterns=/integration/", - "test:verbose": "NODE_OPTIONS='--experimental-vm-modules' jest --config jest.config.cjs --verbose --passWithNoTests --testPathIgnorePatterns=/integration/", - "test:ci": "NODE_OPTIONS='--experimental-vm-modules' jest --config jest.config.cjs --coverage --ci --maxWorkers=2 --passWithNoTests --testPathIgnorePatterns=/integration/" + "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --config jest.config.cjs --passWithNoTests --testPathIgnorePatterns=/integration/", + "test:watch": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --config jest.config.cjs --watch --testPathIgnorePatterns=/integration/", + "test:coverage": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --config jest.config.cjs --coverage --passWithNoTests --testPathIgnorePatterns=/integration/", + "test:verbose": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --config jest.config.cjs --verbose --passWithNoTests --testPathIgnorePatterns=/integration/", + "test:ci": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --config jest.config.cjs --coverage --ci --maxWorkers=2 --passWithNoTests --testPathIgnorePatterns=/integration/" }, "keywords": [ "agents", @@ -50,6 +50,7 @@ "@types/node": "catalog:", "@typescript-eslint/eslint-plugin": "catalog:", "@typescript-eslint/parser": "catalog:", + "cross-env": "catalog:", "eslint": "catalog:", "jest": "catalog:", "js-yaml": "catalog:", From 328a5af1d40a4df863942713dd982911e38dfc90 Mon Sep 17 00:00:00 2001 From: Johan Broberg Date: Tue, 30 Dec 2025 09:37:09 -0800 Subject: [PATCH 11/14] Update packages/agents-a365-tooling/src/McpToolServerConfigurationService.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/McpToolServerConfigurationService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/agents-a365-tooling/src/McpToolServerConfigurationService.ts b/packages/agents-a365-tooling/src/McpToolServerConfigurationService.ts index e281a8cd..5a693a29 100644 --- a/packages/agents-a365-tooling/src/McpToolServerConfigurationService.ts +++ b/packages/agents-a365-tooling/src/McpToolServerConfigurationService.ts @@ -166,7 +166,8 @@ export class McpToolServerConfigurationService { } return { mcpServerName: serverName, - url: s.url || Utility.BuildMcpServerUrl(serverName) + url: s.url || Utility.BuildMcpServerUrl(serverName), + headers: s.headers }; }); } catch (err: unknown) { From 4a03d0b48ea08b9fc63ea035ebcd6521e7167caf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Dec 2025 17:43:03 +0000 Subject: [PATCH 12/14] Add test coverage for custom headers in manifest Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> --- .../McpToolServerConfigurationService.test.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/tooling/McpToolServerConfigurationService.test.ts b/tests/tooling/McpToolServerConfigurationService.test.ts index ce139778..ce9b20c7 100644 --- a/tests/tooling/McpToolServerConfigurationService.test.ts +++ b/tests/tooling/McpToolServerConfigurationService.test.ts @@ -220,5 +220,47 @@ describe('McpToolServerConfigurationService', () => { expect.stringContaining('Either mcpServerName or mcpServerUniqueName must be provided') ); }); + + it('should preserve custom headers when provided in manifest', async () => { + // Arrange + const manifestContent = { + mcpServers: [ + { + mcpServerName: 'serverWithHeaders', + url: 'http://localhost:3000/custom-mcp', + headers: { + 'Authorization': 'Bearer token123', + 'X-Custom-Header': 'custom-value' + } + }, + { + mcpServerName: 'serverWithoutHeaders', + url: 'http://localhost:4000/another-mcp' + } + ] + }; + + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(manifestContent)); + + // Act + const servers = await service.listToolServers('test-agent-id', 'mock-auth-token'); + + // Assert + expect(servers).toHaveLength(2); + + // First server should have headers preserved + expect(servers[0].mcpServerName).toBe('serverWithHeaders'); + expect(servers[0].url).toBe('http://localhost:3000/custom-mcp'); + expect(servers[0].headers).toEqual({ + 'Authorization': 'Bearer token123', + 'X-Custom-Header': 'custom-value' + }); + + // Second server should have undefined headers + expect(servers[1].mcpServerName).toBe('serverWithoutHeaders'); + expect(servers[1].url).toBe('http://localhost:4000/another-mcp'); + expect(servers[1].headers).toBeUndefined(); + }); }); }); From 66515069644b11efc52c9351ef257229b5c0ac74 Mon Sep 17 00:00:00 2001 From: Johan Broberg Date: Thu, 22 Jan 2026 20:17:33 -0800 Subject: [PATCH 13/14] Refactor MCPServerManifestEntry type definition and update test scripts to use cross-env for consistency --- packages/agents-a365-tooling/src/contracts.ts | 9 +++++---- tests/package.json | 10 +++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/agents-a365-tooling/src/contracts.ts b/packages/agents-a365-tooling/src/contracts.ts index fe188893..8a737527 100644 --- a/packages/agents-a365-tooling/src/contracts.ts +++ b/packages/agents-a365-tooling/src/contracts.ts @@ -7,12 +7,13 @@ export interface MCPServerConfig { headers?: Record; } -export interface MCPServerManifestEntry { - mcpServerName?: string; - mcpServerUniqueName?: string; +export type MCPServerManifestEntry = { url?: string; headers?: Record; -} +} & ( + | { mcpServerName: string; mcpServerUniqueName?: string } + | { mcpServerUniqueName: string; mcpServerName?: string } +); export interface McpClientTool { name: string; diff --git a/tests/package.json b/tests/package.json index 2d962391..065306e0 100644 --- a/tests/package.json +++ b/tests/package.json @@ -6,11 +6,11 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { - "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/" + "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --config jest.config.cjs --passWithNoTests --testPathIgnorePatterns=/integration/", + "test:watch": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --config jest.config.cjs --watch --testPathIgnorePatterns=/integration/", + "test:coverage": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --config jest.config.cjs --coverage --passWithNoTests --testPathIgnorePatterns=/integration/", + "test:verbose": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --config jest.config.cjs --verbose --passWithNoTests --testPathIgnorePatterns=/integration/", + "test:ci": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --config jest.config.cjs --coverage --ci --maxWorkers=2 --passWithNoTests --testPathIgnorePatterns=/integration/" }, "keywords": [ "agents", From f7303ae3918e09f97777d325dcd3b35557c48df4 Mon Sep 17 00:00:00 2001 From: Johan Broberg Date: Mon, 26 Jan 2026 08:55:21 -0800 Subject: [PATCH 14/14] Remove unused MCP server configuration comments Removed commented-out MCP server configuration. --- .../src/McpToolServerConfigurationService.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/agents-a365-tooling/src/McpToolServerConfigurationService.ts b/packages/agents-a365-tooling/src/McpToolServerConfigurationService.ts index ef232fbd..b8bff0a2 100644 --- a/packages/agents-a365-tooling/src/McpToolServerConfigurationService.ts +++ b/packages/agents-a365-tooling/src/McpToolServerConfigurationService.ts @@ -234,10 +234,6 @@ export class McpToolServerConfigurationService { * { * "mcpServerName": "sharePointMCPServerConfig", * "mcpServerUniqueName": "mcp_SharePointTools" - * }, - * { - * "mcpServerName": "customMCPServer", - * "url": "http://localhost:3000/mcp" * } * ] * }