diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 00000000..7a357288 --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,81 @@ +import { AzureCliCredential, ChainedTokenCredential, DefaultAzureCredential, TokenCredential } from "@azure/identity"; +import { AccountInfo, AuthenticationResult, PublicClientApplication } from "@azure/msal-node"; +import open from "open"; + +const scopes = ["499b84ac-1321-427f-aa17-267ca6975798/.default"]; + +class OAuthAuthenticator { + static clientId = "ac9c72b1-86e4-4849-be22-eaae7731117a"; + static authority = "https://login.microsoftonline.com/common"; + + private accountId: AccountInfo | null; + private publicClientApp: PublicClientApplication; + + constructor() { + this.accountId = null; + this.publicClientApp = new PublicClientApplication({ + auth: { + clientId: OAuthAuthenticator.clientId, + authority: OAuthAuthenticator.authority, + }, + }); + } + + public async getToken(): Promise { + let authResult: AuthenticationResult | null = null; + if (this.accountId) { + try { + authResult = await this.publicClientApp.acquireTokenSilent({ + scopes, + account: this.accountId, + }); + } catch (error) { + authResult = null; + } + } + if (!authResult) { + authResult = await this.publicClientApp.acquireTokenInteractive({ + scopes, + openBrowser: async (url) => { + open(url); + }, + }); + this.accountId = authResult.account; + } + + if (!authResult.accessToken) { + throw new Error("Failed to obtain Azure DevOps OAuth token."); + } + return authResult.accessToken; + } +} + +function createAuthenticator(type: string, tenantId?: string): () => Promise { + switch (type) { + case "azcli": + case "env": + if (type !== "env") { + process.env.AZURE_TOKEN_CREDENTIALS = "dev"; + } + let credential: TokenCredential = new DefaultAzureCredential(); // CodeQL [SM05138] resolved by explicitly setting AZURE_TOKEN_CREDENTIALS + if (tenantId) { + // Use Azure CLI credential if tenantId is provided for multi-tenant scenarios + const azureCliCredential = new AzureCliCredential({ tenantId }); + credential = new ChainedTokenCredential(azureCliCredential, credential); + } + return async () => { + const result = await credential.getToken(scopes); + if (!result) { + throw new Error("Failed to obtain Azure DevOps token. Ensure you have Azure CLI logged or use interactive type of authentication."); + } + return result.token; + }; + + default: + const authenticator = new OAuthAuthenticator(); + return () => { + return authenticator.getToken(); + }; + } +} +export { createAuthenticator }; diff --git a/src/index.ts b/src/index.ts index 79cc9a32..e3c97e0b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,10 +6,10 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import * as azdev from "azure-devops-node-api"; -import { AccessToken, AzureCliCredential, ChainedTokenCredential, DefaultAzureCredential, TokenCredential } from "@azure/identity"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; +import { createAuthenticator } from "./auth.js"; import { configurePrompts } from "./prompts.js"; import { configureAllTools } from "./tools.js"; import { UserAgentComposer } from "./useragent.js"; @@ -26,42 +26,28 @@ const argv = yargs(hideBin(process.argv)) type: "string", }); }) + .option("authentication", { + alias: "a", + describe: "Type of authentication to use. Supported values are 'interactive', 'azcli' and 'env' (default: 'interactive')", + type: "string", + choices: ["interactive", "azcli", "env"], + default: "interactive", + }) .option("tenant", { alias: "t", - describe: "Azure tenant ID (optional, required for multi-tenant scenarios)", + describe: "Azure tenant ID (optional, applied only when using 'azcli' type of authentication)", type: "string", }) .help() .parseSync(); export const orgName = argv.organization as string; -const tenantId = argv.tenant; const orgUrl = "https://dev.azure.com/" + orgName; -async function getAzureDevOpsToken(): Promise { - if (process.env.ADO_MCP_AZURE_TOKEN_CREDENTIALS) { - process.env.AZURE_TOKEN_CREDENTIALS = process.env.ADO_MCP_AZURE_TOKEN_CREDENTIALS; - } else { - process.env.AZURE_TOKEN_CREDENTIALS = "dev"; - } - let credential: TokenCredential = new DefaultAzureCredential(); // CodeQL [SM05138] resolved by explicitly setting AZURE_TOKEN_CREDENTIALS - if (tenantId) { - // Use Azure CLI credential if tenantId is provided for multi-tenant scenarios - const azureCliCredential = new AzureCliCredential({ tenantId }); - credential = new ChainedTokenCredential(azureCliCredential, credential); - } - - const token = await credential.getToken("499b84ac-1321-427f-aa17-267ca6975798/.default"); - if (!token) { - throw new Error("Failed to obtain Azure DevOps token. Ensure you have Azure CLI logged in or another token source setup correctly."); - } - return token; -} - -function getAzureDevOpsClient(userAgentComposer: UserAgentComposer): () => Promise { +function getAzureDevOpsClient(getAzureDevOpsToken: () => Promise, userAgentComposer: UserAgentComposer): () => Promise { return async () => { - const token = await getAzureDevOpsToken(); - const authHandler = azdev.getBearerHandler(token.token); + const accessToken = await getAzureDevOpsToken(); + const authHandler = azdev.getBearerHandler(accessToken); const connection = new azdev.WebApi(orgUrl, authHandler, undefined, { productName: "AzureDevOps.MCP", productVersion: packageVersion, @@ -81,10 +67,11 @@ async function main() { server.server.oninitialized = () => { userAgentComposer.appendMcpClientInfo(server.server.getClientVersion()); }; + const authenticator = createAuthenticator(argv.authentication, argv.tenant); configurePrompts(server); - configureAllTools(server, getAzureDevOpsToken, getAzureDevOpsClient(userAgentComposer), () => userAgentComposer.userAgent); + configureAllTools(server, authenticator, getAzureDevOpsClient(authenticator, userAgentComposer), () => userAgentComposer.userAgent); const transport = new StdioServerTransport(); await server.connect(transport); diff --git a/src/tools.ts b/src/tools.ts index 35f9bcb8..2239033b 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -2,7 +2,6 @@ // Licensed under the MIT License. import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { AccessToken } from "@azure/identity"; import { WebApi } from "azure-devops-node-api"; import { configureCoreTools } from "./tools/core.js"; @@ -15,7 +14,7 @@ import { configureWikiTools } from "./tools/wiki.js"; import { configureTestPlanTools } from "./tools/testplans.js"; import { configureSearchTools } from "./tools/search.js"; -function configureAllTools(server: McpServer, tokenProvider: () => Promise, connectionProvider: () => Promise, userAgentProvider: () => string) { +function configureAllTools(server: McpServer, tokenProvider: () => Promise, connectionProvider: () => Promise, userAgentProvider: () => string) { configureCoreTools(server, tokenProvider, connectionProvider); configureWorkTools(server, tokenProvider, connectionProvider); configureBuildTools(server, tokenProvider, connectionProvider); diff --git a/src/tools/auth.ts b/src/tools/auth.ts index e8b758c4..faed47c1 100644 --- a/src/tools/auth.ts +++ b/src/tools/auth.ts @@ -4,10 +4,10 @@ import { AccessToken } from "@azure/identity"; import { WebApi } from "azure-devops-node-api"; -async function getCurrentUserDetails(tokenProvider: () => Promise, connectionProvider: () => Promise) { +async function getCurrentUserDetails(tokenProvider: () => Promise, connectionProvider: () => Promise) { const connection = await connectionProvider(); const url = `${connection.serverUrl}/_apis/connectionData`; - const token = (await tokenProvider()).token; + const token = await tokenProvider(); const response = await fetch(url, { method: "GET", headers: { diff --git a/src/tools/builds.ts b/src/tools/builds.ts index 27d583c0..ec2ae2e5 100644 --- a/src/tools/builds.ts +++ b/src/tools/builds.ts @@ -21,7 +21,7 @@ const BUILD_TOOLS = { update_build_stage: "build_update_build_stage", }; -function configureBuildTools(server: McpServer, tokenProvider: () => Promise, connectionProvider: () => Promise) { +function configureBuildTools(server: McpServer, tokenProvider: () => Promise, connectionProvider: () => Promise) { server.tool( BUILD_TOOLS.get_definitions, "Retrieves a list of build definitions for a given project.", @@ -339,7 +339,7 @@ function configureBuildTools(server: McpServer, tokenProvider: () => Promise project.name?.toLowerCase().includes(lowerCaseFilter)); } -function configureCoreTools(server: McpServer, tokenProvider: () => Promise, connectionProvider: () => Promise) { +function configureCoreTools(server: McpServer, tokenProvider: () => Promise, connectionProvider: () => Promise) { server.tool( CORE_TOOLS.list_project_teams, "Retrieve a list of teams for the specified Azure DevOps project.", @@ -112,7 +112,7 @@ function configureCoreTools(server: McpServer, tokenProvider: () => Promise Promise, connectionProvider: () => Promise) { +function configureReleaseTools(server: McpServer, _: () => Promise, connectionProvider: () => Promise) { server.tool( RELEASE_TOOLS.get_release_definitions, "Retrieves list of release definitions for a given project.", diff --git a/src/tools/repos.ts b/src/tools/repos.ts index 9fbff5e3..8c120069 100644 --- a/src/tools/repos.ts +++ b/src/tools/repos.ts @@ -97,7 +97,7 @@ function filterReposByName(repositories: GitRepository[], repoNameFilter: string return filteredByName; } -function configureRepoTools(server: McpServer, tokenProvider: () => Promise, connectionProvider: () => Promise) { +function configureRepoTools(server: McpServer, tokenProvider: () => Promise, connectionProvider: () => Promise) { server.tool( REPO_TOOLS.create_pull_request, "Create a new pull request.", diff --git a/src/tools/search.ts b/src/tools/search.ts index 2d741a75..b2fcc651 100644 --- a/src/tools/search.ts +++ b/src/tools/search.ts @@ -17,7 +17,7 @@ const SEARCH_TOOLS = { search_workitem: "search_workitem", }; -function configureSearchTools(server: McpServer, tokenProvider: () => Promise, connectionProvider: () => Promise, userAgentProvider: () => string) { +function configureSearchTools(server: McpServer, tokenProvider: () => Promise, connectionProvider: () => Promise, userAgentProvider: () => string) { server.tool( SEARCH_TOOLS.search_code, "Search Azure DevOps Repositories for a given search text", @@ -57,7 +57,7 @@ function configureSearchTools(server: McpServer, tokenProvider: () => Promise Promise Promise Promise, connectionProvider: () => Promise) { +function configureTestPlanTools(server: McpServer, _: () => Promise, connectionProvider: () => Promise) { /* LIST OF TEST PLANS get list of test plans by project diff --git a/src/tools/wiki.ts b/src/tools/wiki.ts index 59367625..f062ba9f 100644 --- a/src/tools/wiki.ts +++ b/src/tools/wiki.ts @@ -14,7 +14,7 @@ const WIKI_TOOLS = { get_wiki_page_content: "wiki_get_page_content", }; -function configureWikiTools(server: McpServer, tokenProvider: () => Promise, connectionProvider: () => Promise) { +function configureWikiTools(server: McpServer, _: () => Promise, connectionProvider: () => Promise) { server.tool( WIKI_TOOLS.get_wiki, "Get the wiki by wikiIdentifier", diff --git a/src/tools/work.ts b/src/tools/work.ts index b397d318..b46a77cb 100644 --- a/src/tools/work.ts +++ b/src/tools/work.ts @@ -13,7 +13,7 @@ const WORK_TOOLS = { assign_iterations: "work_assign_iterations", }; -function configureWorkTools(server: McpServer, tokenProvider: () => Promise, connectionProvider: () => Promise) { +function configureWorkTools(server: McpServer, _: () => Promise, connectionProvider: () => Promise) { server.tool( WORK_TOOLS.list_team_iterations, "Retrieve a list of iterations for a specific team in a project.", diff --git a/src/tools/workitems.ts b/src/tools/workitems.ts index d455c67d..ede3d560 100644 --- a/src/tools/workitems.ts +++ b/src/tools/workitems.ts @@ -58,7 +58,7 @@ function getLinkTypeFromName(name: string) { } } -function configureWorkItemTools(server: McpServer, tokenProvider: () => Promise, connectionProvider: () => Promise, userAgentProvider: () => string) { +function configureWorkItemTools(server: McpServer, tokenProvider: () => Promise, connectionProvider: () => Promise, userAgentProvider: () => string) { server.tool( WORKITEM_TOOLS.list_backlogs, "Revieve a list of backlogs for a given project and team.", @@ -205,7 +205,7 @@ function configureWorkItemTools(server: McpServer, tokenProvider: () => Promise< const response = await fetch(`${orgUrl}/${project}/_apis/wit/workItems/${workItemId}/comments?format=${formatParameter}&api-version=${markdownCommentsApiVersion}`, { method: "POST", headers: { - "Authorization": `Bearer ${accessToken.token}`, + "Authorization": `Bearer ${accessToken}`, "Content-Type": "application/json", "User-Agent": userAgentProvider(), }, @@ -329,7 +329,7 @@ function configureWorkItemTools(server: McpServer, tokenProvider: () => Promise< const response = await fetch(`${orgUrl}/_apis/wit/$batch?api-version=${batchApiVersion}`, { method: "PATCH", headers: { - "Authorization": `Bearer ${accessToken.token}`, + "Authorization": `Bearer ${accessToken}`, "Content-Type": "application/json", "User-Agent": userAgentProvider(), }, @@ -668,7 +668,7 @@ function configureWorkItemTools(server: McpServer, tokenProvider: () => Promise< const response = await fetch(`${orgUrl}/_apis/wit/$batch?api-version=${batchApiVersion}`, { method: "PATCH", headers: { - "Authorization": `Bearer ${accessToken.token}`, + "Authorization": `Bearer ${accessToken}`, "Content-Type": "application/json", "User-Agent": userAgentProvider(), }, @@ -740,7 +740,7 @@ function configureWorkItemTools(server: McpServer, tokenProvider: () => Promise< const response = await fetch(`${orgUrl}/_apis/wit/$batch?api-version=${batchApiVersion}`, { method: "PATCH", headers: { - "Authorization": `Bearer ${accessToken.token}`, + "Authorization": `Bearer ${accessToken}`, "Content-Type": "application/json", "User-Agent": userAgentProvider(), }, diff --git a/test/src/tools/builds.test.ts b/test/src/tools/builds.test.ts index 215b3a04..549d1946 100644 --- a/test/src/tools/builds.test.ts +++ b/test/src/tools/builds.test.ts @@ -10,7 +10,7 @@ import { mockUpdateBuildStageResponse } from "../../mocks/builds"; // Mock fetch globally global.fetch = jest.fn() as jest.MockedFunction; -type TokenProviderMock = () => Promise; +type TokenProviderMock = () => Promise; type ConnectionProviderMock = () => Promise; describe("configureBuildTools", () => { @@ -46,8 +46,7 @@ describe("configureBuildTools", () => { const [, , , handler] = call; // Mock the token provider - const mockToken = { token: "mock-token" }; - (tokenProvider as jest.Mock).mockResolvedValue(mockToken); + (tokenProvider as jest.Mock).mockResolvedValue("mock-token"); // Mock successful fetch response const mockResponse = { @@ -88,8 +87,7 @@ describe("configureBuildTools", () => { const [, , , handler] = call; // Mock the token provider - const mockToken = { token: "mock-token" }; - (tokenProvider as jest.Mock).mockResolvedValue(mockToken); + (tokenProvider as jest.Mock).mockResolvedValue("mock-token"); // Mock failed fetch response const mockResponse = { @@ -129,8 +127,7 @@ describe("configureBuildTools", () => { const [, , , handler] = call; // Mock the token provider - const mockToken = { token: "mock-token" }; - (tokenProvider as jest.Mock).mockResolvedValue(mockToken); + (tokenProvider as jest.Mock).mockResolvedValue("mock-token"); // Mock network error const networkError = new Error("Network connection failed"); @@ -189,8 +186,7 @@ describe("configureBuildTools", () => { if (!call) throw new Error("build_update_build_stage tool not registered"); const [, , , handler] = call; - const mockToken = { token: "mock-token" }; - (tokenProvider as jest.Mock).mockResolvedValue(mockToken); + (tokenProvider as jest.Mock).mockResolvedValue("mock-token"); const mockResponse = { ok: true, diff --git a/test/src/tools/core.test.ts b/test/src/tools/core.test.ts index 9f22da4f..a3f4c19c 100644 --- a/test/src/tools/core.test.ts +++ b/test/src/tools/core.test.ts @@ -4,7 +4,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { configureCoreTools } from "../../../src/tools/core"; import { WebApi } from "azure-devops-node-api"; -type TokenProviderMock = () => Promise; +type TokenProviderMock = () => Promise; type ConnectionProviderMock = () => Promise; interface CoreApiMock { @@ -432,7 +432,7 @@ describe("configureCoreTools", () => { const [, , , handler] = call; // Mock token provider - (tokenProvider as jest.Mock).mockResolvedValue({ token: "fake-token" }); + (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); // Mock connection with serverUrl const mockConnectionWithUrl = { @@ -496,7 +496,7 @@ describe("configureCoreTools", () => { if (!call) throw new Error("core_get_identity_ids tool not registered"); const [, , , handler] = call; - (tokenProvider as jest.Mock).mockResolvedValue({ token: "fake-token" }); + (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); const mockConnectionWithUrl = { ...mockConnection, serverUrl: "https://dev.azure.com/test-org", @@ -524,7 +524,7 @@ describe("configureCoreTools", () => { if (!call) throw new Error("core_get_identity_ids tool not registered"); const [, , , handler] = call; - (tokenProvider as jest.Mock).mockResolvedValue({ token: "fake-token" }); + (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); const mockConnectionWithUrl = { ...mockConnection, serverUrl: "https://dev.azure.com/test-org", @@ -551,7 +551,7 @@ describe("configureCoreTools", () => { if (!call) throw new Error("core_get_identity_ids tool not registered"); const [, , , handler] = call; - (tokenProvider as jest.Mock).mockResolvedValue({ token: "fake-token" }); + (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); const mockConnectionWithUrl = { ...mockConnection, serverUrl: "https://dev.azure.com/test-org", @@ -578,7 +578,7 @@ describe("configureCoreTools", () => { if (!call) throw new Error("core_get_identity_ids tool not registered"); const [, , , handler] = call; - (tokenProvider as jest.Mock).mockResolvedValue({ token: "fake-token" }); + (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); const mockConnectionWithUrl = { ...mockConnection, serverUrl: "https://dev.azure.com/test-org", @@ -602,7 +602,7 @@ describe("configureCoreTools", () => { if (!call) throw new Error("core_get_identity_ids tool not registered"); const [, , , handler] = call; - (tokenProvider as jest.Mock).mockResolvedValue({ token: "fake-token" }); + (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); const mockConnectionWithUrl = { ...mockConnection, serverUrl: "https://dev.azure.com/test-org", diff --git a/test/src/tools/testplan.test.ts b/test/src/tools/testplan.test.ts index 97a6884b..8940bbca 100644 --- a/test/src/tools/testplan.test.ts +++ b/test/src/tools/testplan.test.ts @@ -8,7 +8,7 @@ import { ITestResultsApi } from "azure-devops-node-api/TestResultsApi"; import { IWorkItemTrackingApi } from "azure-devops-node-api/WorkItemTrackingApi"; import { ITestApi } from "azure-devops-node-api/TestApi"; -type TokenProviderMock = () => Promise; +type TokenProviderMock = () => Promise; type ConnectionProviderMock = () => Promise; describe("configureTestPlanTools", () => { diff --git a/test/src/tools/wiki.test.ts b/test/src/tools/wiki.test.ts index b9df854d..1ef53999 100644 --- a/test/src/tools/wiki.test.ts +++ b/test/src/tools/wiki.test.ts @@ -4,7 +4,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { WebApi } from "azure-devops-node-api"; import { configureWikiTools } from "../../../src/tools/wiki"; -type TokenProviderMock = () => Promise; +type TokenProviderMock = () => Promise; type ConnectionProviderMock = () => Promise; interface WikiApiMock { getWiki: jest.Mock; diff --git a/test/src/tools/work.test.ts b/test/src/tools/work.test.ts index 395c0c8c..e7e7ed28 100644 --- a/test/src/tools/work.test.ts +++ b/test/src/tools/work.test.ts @@ -5,7 +5,7 @@ import { configureWorkTools } from "../../../src/tools/work"; import { WebApi } from "azure-devops-node-api"; import { TreeStructureGroup } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces"; -type TokenProviderMock = () => Promise; +type TokenProviderMock = () => Promise; type ConnectionProviderMock = () => Promise; interface WorkApiMock { diff --git a/test/src/tools/workitems.test.ts b/test/src/tools/workitems.test.ts index 751d655d..e7e9c0e8 100644 --- a/test/src/tools/workitems.test.ts +++ b/test/src/tools/workitems.test.ts @@ -16,7 +16,7 @@ import { _mockWorkItemType, } from "../../mocks/work-items"; -type TokenProviderMock = () => Promise; +type TokenProviderMock = () => Promise; type ConnectionProviderMock = () => Promise; interface WorkApiMock { @@ -356,7 +356,7 @@ describe("configureWorkItemTools", () => { const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; - (tokenProvider as jest.Mock).mockResolvedValue({ token: "fake-token" }); + (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); // Mock fetch for the API call const mockFetch = jest.fn().mockResolvedValue({ @@ -396,7 +396,7 @@ describe("configureWorkItemTools", () => { const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; - (tokenProvider as jest.Mock).mockResolvedValue({ token: "fake-token" }); + (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); // Mock fetch for the API call const mockFetch = jest.fn().mockResolvedValue({ @@ -437,7 +437,7 @@ describe("configureWorkItemTools", () => { const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; - (tokenProvider as jest.Mock).mockResolvedValue({ token: "fake-token" }); + (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); // Mock fetch for the API call const mockFetch = jest.fn().mockResolvedValue({ @@ -961,7 +961,7 @@ describe("configureWorkItemTools", () => { mockConnection.serverUrl = "https://dev.azure.com/contoso"; // Mock tokenProvider for this test - (tokenProvider as jest.Mock).mockResolvedValue({ token: "fake-token" }); + (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); // Mock fetch for successful response global.fetch = jest.fn().mockResolvedValue({ @@ -1025,7 +1025,7 @@ describe("configureWorkItemTools", () => { const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; - (tokenProvider as jest.Mock).mockResolvedValue({ token: "fake-token" }); + (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); global.fetch = jest.fn().mockResolvedValue({ ok: true, @@ -1090,7 +1090,7 @@ describe("configureWorkItemTools", () => { const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; - (tokenProvider as jest.Mock).mockResolvedValue({ token: "fake-token" }); + (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); global.fetch = jest.fn().mockResolvedValue({ ok: true, @@ -1160,7 +1160,7 @@ describe("configureWorkItemTools", () => { const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; - (tokenProvider as jest.Mock).mockResolvedValue({ token: "fake-token" }); + (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); global.fetch = jest.fn().mockResolvedValue({ ok: false, @@ -1191,7 +1191,7 @@ describe("configureWorkItemTools", () => { const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; - (tokenProvider as jest.Mock).mockResolvedValue({ token: "fake-token" }); + (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); global.fetch = jest.fn().mockResolvedValue({ ok: true, @@ -1234,7 +1234,7 @@ describe("configureWorkItemTools", () => { const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; - (tokenProvider as jest.Mock).mockResolvedValue({ token: "fake-token" }); + (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); global.fetch = jest.fn().mockResolvedValue({ ok: false, @@ -1380,7 +1380,7 @@ describe("configureWorkItemTools", () => { const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; - (tokenProvider as jest.Mock).mockResolvedValue({ token: "fake-token" }); + (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); global.fetch = jest.fn().mockResolvedValue({ ok: true, @@ -1416,7 +1416,7 @@ describe("configureWorkItemTools", () => { const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; - (tokenProvider as jest.Mock).mockResolvedValue({ token: "fake-token" }); + (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); // Mock fetch for the batch API call const mockFetch = jest.fn().mockResolvedValue({ @@ -1462,7 +1462,7 @@ describe("configureWorkItemTools", () => { const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; - (tokenProvider as jest.Mock).mockResolvedValue({ token: "fake-token" }); + (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); // Mock fetch for the batch API call const mockFetch = jest.fn().mockResolvedValue({ @@ -1511,7 +1511,7 @@ describe("configureWorkItemTools", () => { const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; - (tokenProvider as jest.Mock).mockResolvedValue({ token: "fake-token" }); + (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); // Create 51 items to exceed the limit const items = Array.from({ length: 51 }, (_, i) => ({ @@ -1540,7 +1540,7 @@ describe("configureWorkItemTools", () => { const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; - (tokenProvider as jest.Mock).mockResolvedValue({ token: "fake-token" }); + (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); // Mock fetch for the batch API call const mockFetch = jest.fn().mockResolvedValue({ @@ -1588,7 +1588,7 @@ describe("configureWorkItemTools", () => { const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; - (tokenProvider as jest.Mock).mockResolvedValue({ token: "fake-token" }); + (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); // Mock fetch for a failed response const mockFetch = jest.fn().mockResolvedValue({