@sonyjv/azure-devops-mcp 2.9.0-onprem.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Microsoft Corporation.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE
package/README.md ADDED
@@ -0,0 +1,234 @@
1
+ # Azure DevOps MCP Server
2
+
3
+ > [!NOTE]
4
+ > **This is a fork.** [sonyjv/azure-devops-mcp](https://github.com/sonyjv/azure-devops-mcp) is a personal fork of [microsoft/azure-devops-mcp](https://github.com/microsoft/azure-devops-mcp), the official Azure DevOps MCP Server. It adds support for connecting to an **on-premises Azure DevOps Server / TFS collection**, in addition to Azure DevOps Services (cloud) — see [Azure DevOps Server (On-Premises)](./docs/GETTINGSTARTED.md#azure-devops-server-on-premises).
5
+ >
6
+ > This fork is published to npm as [`@sonyjv/azure-devops-mcp`](https://www.npmjs.com/package/@sonyjv/azure-devops-mcp) (Microsoft's own `@azure-devops/mcp` name is upstream's) — run it with `npx -y @sonyjv/azure-devops-mcp`, no separate clone or build step needed. It is not intended to be merged upstream. See [Local MCP Server Installation](#local-mcp-server-installation-optional).
7
+
8
+ > [!WARNING]
9
+ > We recently completed a full tool consolidation that includes renaming of existing tools. Please see the [Toolset documentation](docs/TOOLSET.md) for the complete list of new tool names.
10
+
11
+ This project gives AI agents access to Azure DevOps through the Model Context Protocol (MCP). Use the hosted remote server for the simplest setup, or run the local server when you need a `stdio` connection — or when you need on-premises Azure DevOps Server support, which the hosted remote server (below) does not provide.
12
+
13
+ ## Table of Contents
14
+
15
+ > [!IMPORTANT]
16
+ > If you're on Azure DevOps Services (cloud) and don't need on-premises support, Microsoft's [Remote MCP Server](https://learn.microsoft.com/en-us/azure/devops/mcp-server/remote-mcp-server) requires no installation and gets new features first — see [Learn more](#remote-mcp-server-recommended). It does **not** support on-premises Azure DevOps Server, which is this fork's reason for existing — on-prem users need the local server below.
17
+
18
+ 1. [Overview](#overview)
19
+ 2. [Design](#design)
20
+ 3. [Remote MCP Server (Recommended)](#remote-mcp-server-recommended)
21
+ 4. [Supported Tools](#supported-tools)
22
+ 5. [Local MCP Server Installation (Optional)](#local-mcp-server-installation-optional)
23
+ 6. [Using Domains (Local Server)](#using-domains-local-server)
24
+ 7. [Project and Team Defaults (Local Server)](#project-and-team-defaults-local-server)
25
+ 8. [Troubleshooting](#troubleshooting)
26
+ 9. [Examples and Best Practices](#examples-and-best-practices)
27
+ 10. [Frequently Asked Questions](#frequently-asked-questions)
28
+ 11. [Contributing](#contributing)
29
+
30
+ ## Overview
31
+
32
+ The Azure DevOps MCP Server brings Azure DevOps context to your agents. Try prompts like:
33
+
34
+ - "List my ADO projects"
35
+ - "List ADO Builds for 'Contoso'"
36
+ - "List ADO Repos for 'Contoso'"
37
+ - "List test plans for 'Contoso'"
38
+ - "List teams for project 'Contoso'"
39
+ - "List iterations for project 'Contoso'"
40
+ - "List my work items for project 'Contoso'"
41
+ - "List work items in current iteration for 'Contoso' project and 'Contoso Team'"
42
+ - "List all wikis in the 'Contoso' project"
43
+ - "Create a wiki page '/Architecture/Overview' with content about system design"
44
+ - "Update the wiki page '/Getting Started' with new onboarding instructions"
45
+ - "Get the content of the wiki page '/API/Authentication' from the Documentation wiki"
46
+
47
+ ## Design
48
+
49
+ Each tool handles a focused Azure DevOps task. The server provides a thin layer over the REST APIs, while the AI agent handles higher-level reasoning.
50
+
51
+ ## Remote MCP Server (Recommended)
52
+
53
+ For complete instructions, see the [Remote MCP Server onboarding documentation](https://learn.microsoft.com/en-us/azure/devops/mcp-server/remote-mcp-server?view=azure-devops).
54
+
55
+ The remote server will eventually replace the local server. The local server remains supported, but new development will focus on the remote server. Existing local server users should begin planning their migration.
56
+
57
+ If you encounter issues with tools, need support, or have a feature request, you can report an issue using the [Remote MCP Server issue template](https://github.com/microsoft/azure-devops-mcp/issues/new?template=remote-mcp-server-issue.md). During the preview period, we will track Remote MCP Server issues through this repository.
58
+
59
+ ### Quick Start
60
+
61
+ Create `.vscode/mcp.json` in your project and add this configuration. Replace `{organization}` with your Azure DevOps organization name.
62
+
63
+ ```json
64
+ {
65
+ "servers": {
66
+ "ado-remote-mcp": {
67
+ "url": "https://mcp.dev.azure.com/{organization}",
68
+ "type": "http"
69
+ }
70
+ },
71
+ "inputs": []
72
+ }
73
+ ```
74
+
75
+ See the [remote server configuration documentation](https://learn.microsoft.com/en-us/azure/devops/mcp-server/remote-mcp-server?view=azure-devops#mcpjson-configuration) for more options.
76
+
77
+ After saving `.vscode/mcp.json`, start the server from the MCP view in VS Code, then run a prompt like `List ADO projects`.
78
+
79
+ ## Supported Tools
80
+
81
+ See the [Available Tools](https://learn.microsoft.com/en-us/azure/devops/mcp-server/remote-mcp-server?view=azure-devops#available-tools) documentation for the complete list of available remote tools.
82
+
83
+ For the complete list of local tools, see [TOOLSET.md](./docs/TOOLSET.md).
84
+
85
+ ## Local MCP Server Installation (Optional)
86
+
87
+ > [!NOTE]
88
+ > This fork publishes under its own package name, `@sonyjv/azure-devops-mcp` — `npx -y @azure-devops/mcp` (as documented for the [upstream project](https://github.com/microsoft/azure-devops-mcp)) installs Microsoft's original package, **not** this fork's on-premises support. `npx -y @sonyjv/azure-devops-mcp` works exactly like installing any published npm package — no clone or build step, and no local `git` needed. If you'd rather always track the unreleased `main` branch instead of a published version, `npx -y github:sonyjv/azure-devops-mcp` also works, though it needs `git` and direct network access to GitHub, which some locked-down/corporate networks block even when npm registry access works fine. Clone-and-build (see [Run from Source](./docs/GETTINGSTARTED.md#run-from-source)) is only needed if you're modifying the code yourself.
89
+
90
+ These steps use Visual Studio Code and GitHub Copilot. For other supported clients, including Visual Studio 2022, Codex, Claude Code, Cursor, OpenCode, and Kilo Code, see the [getting started guide](./docs/GETTINGSTARTED.md). That guide also covers connecting to an on-premises Azure DevOps Server / TFS collection instead of Azure DevOps Services — see [Azure DevOps Server (On-Premises)](./docs/GETTINGSTARTED.md#azure-devops-server-on-premises).
91
+
92
+ ### Prerequisites
93
+
94
+ 1. Install [VS Code](https://code.visualstudio.com/download) or [VS Code Insiders](https://code.visualstudio.com/insiders).
95
+ 2. Install [Node.js 20 or later](https://nodejs.org/en/download).
96
+ 3. Open your project in VS Code.
97
+
98
+ ### Installation
99
+
100
+ #### Install from npm
101
+
102
+ 1. Create `.vscode/mcp.json` in your project.
103
+ 2. Add this configuration:
104
+
105
+ ```json
106
+ {
107
+ "inputs": [
108
+ {
109
+ "id": "ado_org",
110
+ "type": "promptString",
111
+ "description": "Azure DevOps organization name (e.g. 'contoso'), or a full on-premises collection URL (e.g. 'http://tfsserver:8080/tfs/DefaultCollection')"
112
+ }
113
+ ],
114
+ "servers": {
115
+ "ado": {
116
+ "type": "stdio",
117
+ "command": "npx",
118
+ "args": ["-y", "@sonyjv/azure-devops-mcp", "${input:ado_org}"]
119
+ }
120
+ }
121
+ }
122
+ ```
123
+
124
+ 3. Save the file, then start the `ado` server from the MCP view in VS Code.
125
+ 4. Open GitHub Copilot Chat and switch to [Agent mode](https://code.visualstudio.com/blogs/2025/02/24/introducing-copilot-agent-mode).
126
+ 5. Select the Azure DevOps tools, then try a prompt such as `List ADO projects`.
127
+ 6. When prompted, sign in with a Microsoft account that has access to the selected Azure DevOps organization (or configure PAT authentication for an on-premises server — see [Authentication](./docs/GETTINGSTARTED.md#authentication)).
128
+
129
+ To pin to a specific released version instead of always resolving `latest`, replace `@sonyjv/azure-devops-mcp` with `@sonyjv/azure-devops-mcp@<version>` (see the [available versions](https://www.npmjs.com/package/@sonyjv/azure-devops-mcp?activeTab=versions)).
130
+
131
+ For better tool selection, add `.github/copilot-instructions.md` to your project with this instruction:
132
+
133
+ ```text
134
+ This project uses Azure DevOps. Always check whether the Azure DevOps MCP server has a tool relevant to the user's request.
135
+ ```
136
+
137
+ ## Using Domains (Local Server)
138
+
139
+ The local server includes many tools. Domains let you load only the tool groups you need, which keeps the tool list manageable and helps clients with tool limits. Available domains are `core`, `work`, `work-items`, `search`, `test-plans`, `repositories`, `wiki`, `pipelines`, and `advanced-security`.
140
+
141
+ Add `-d` followed by the domains to the server arguments. For example, this configuration loads only work item-related tools:
142
+
143
+ ```json
144
+ {
145
+ "inputs": [
146
+ {
147
+ "id": "ado_org",
148
+ "type": "promptString",
149
+ "description": "Azure DevOps organization name (e.g. 'contoso')"
150
+ }
151
+ ],
152
+ "servers": {
153
+ "ado_with_filtered_domains": {
154
+ "type": "stdio",
155
+ "command": "npx",
156
+ "args": ["-y", "@sonyjv/azure-devops-mcp", "${input:ado_org}", "-d", "core", "work", "work-items"]
157
+ }
158
+ }
159
+ }
160
+ ```
161
+
162
+ Always include `core` so the agent can retrieve project information.
163
+
164
+ > If you omit `-d`, the server loads all domains.
165
+
166
+ ## Project and Team Defaults (Local Server)
167
+
168
+ Set default Azure DevOps project and team values in `.vscode/mcp.json` so tools can skip selection prompts.
169
+
170
+ ### Example `.vscode/mcp.json`
171
+
172
+ ```json
173
+ {
174
+ "servers": {
175
+ "ado": {
176
+ "type": "stdio",
177
+ "command": "npx",
178
+ "args": ["-y", "@sonyjv/azure-devops-mcp", "myorg", "--authentication", "azcli"],
179
+ "env": {
180
+ "ado_mcp_project": "Contoso",
181
+ "ado_mcp_team": "Fabrikam Team"
182
+ }
183
+ }
184
+ }
185
+ }
186
+ ```
187
+
188
+ ## Troubleshooting
189
+
190
+ See the [Troubleshooting guide](./docs/TROUBLESHOOTING.md) for help with common issues and logging.
191
+
192
+ ## Examples
193
+
194
+ See the [examples](./docs/EXAMPLES.md) for sample prompts.
195
+
196
+ ## Frequently Asked Questions
197
+
198
+ For answers to common questions about the Azure DevOps MCP Server, see the [Frequently Asked Questions](./docs/FAQ.md).
199
+
200
+ ## Contributing
201
+
202
+ We welcome contributions. During preview, file issues for bugs, enhancements, or documentation improvements.
203
+
204
+ See our [Contributions Guide](./CONTRIBUTING.md) for:
205
+
206
+ - Development setup
207
+ - Adding new tools
208
+ - Code style and testing
209
+ - Pull request process
210
+
211
+ Read the [Contributions Guide](./CONTRIBUTING.md) before creating a pull request.
212
+
213
+ ## Code of Conduct
214
+
215
+ This project follows the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
216
+ For questions, see the [FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [open@microsoft.com](mailto:open@microsoft.com).
217
+
218
+ ## Hall of Fame
219
+
220
+ Thanks to all contributors who make this project awesome! ❤️
221
+
222
+ [![Contributors](https://contrib.rocks/image?repo=microsoft/azure-devops-mcp)](https://github.com/microsoft/azure-devops-mcp/graphs/contributors)
223
+
224
+ > Generated with [contrib.rocks](https://contrib.rocks)
225
+
226
+ ## License
227
+
228
+ Licensed under the [MIT License](./LICENSE.md).
229
+
230
+ ---
231
+
232
+ _Trademarks: This project may include trademarks or logos for Microsoft or third parties. Use of Microsoft trademarks or logos must follow [Microsoft’s Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). Third-party trademarks are subject to their respective policies._
233
+
234
+ <!-- version: 2023-04-07 [Do not delete this line, it is used for analytics that drive template improvements] -->
package/dist/auth.js ADDED
@@ -0,0 +1,205 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import { AzureCliCredential, ChainedTokenCredential, DefaultAzureCredential } from "@azure/identity";
4
+ import { PublicClientApplication } from "@azure/msal-node";
5
+ import { NativeBrokerPlugin } from "@azure/msal-node-extensions";
6
+ import open from "open";
7
+ import { logger } from "./logger.js";
8
+ const scopes = ["499b84ac-1321-427f-aa17-267ca6975798/.default"];
9
+ const patAllowedHosts = new Set(["dev.azure.com", "vssps.dev.azure.com", "almsearch.dev.azure.com"]);
10
+ function isPatAllowedHost(hostname) {
11
+ const normalizedHostname = hostname.toLowerCase();
12
+ return patAllowedHosts.has(normalizedHostname) || normalizedHostname.endsWith(".visualstudio.com");
13
+ }
14
+ /**
15
+ * Installs a global fetch interceptor that rewrites the Bearer auth header to Basic
16
+ * for requests carrying the PAT.
17
+ *
18
+ * @param rawPat The raw (unencoded) Azure DevOps Personal Access Token.
19
+ * @param configuredHost Hostname of the Azure DevOps connection the user explicitly configured
20
+ * (e.g. via the CLI `organization` argument). Trusted in addition to the built-in cloud allow-list,
21
+ * and — unlike the cloud hosts — allowed over plain `http:` too, since on-premises Azure DevOps
22
+ * Server / TFS collections are frequently reached over an internal network without TLS.
23
+ */
24
+ function installPatFetchInterceptor(rawPat, configuredHost) {
25
+ const originalFetch = globalThis.fetch;
26
+ const patBearerValue = `Bearer ${rawPat}`;
27
+ // HTTP Basic auth requires a username:password pair; Azure DevOps ignores the username for PAT
28
+ // auth, so "PAT" is just a placeholder — matching the literal value azure-devops-node-api's own
29
+ // PersonalAccessTokenCredentialHandler uses internally, for consistency with the requests it sends.
30
+ const basicAuthValue = Buffer.from(`PAT:${rawPat}`).toString("base64");
31
+ const normalizedConfiguredHost = configuredHost?.toLowerCase();
32
+ globalThis.fetch = async (input, init) => {
33
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
34
+ if (headers.get("Authorization") !== patBearerValue) {
35
+ return originalFetch(input, init);
36
+ }
37
+ const requestUrl = new URL(input instanceof Request ? input.url : input.toString());
38
+ const isConfiguredHost = normalizedConfiguredHost !== undefined && requestUrl.hostname.toLowerCase() === normalizedConfiguredHost;
39
+ const schemeAllowed = requestUrl.protocol === "https:" || (requestUrl.protocol === "http:" && isConfiguredHost);
40
+ if (!schemeAllowed || !(isPatAllowedHost(requestUrl.hostname) || isConfiguredHost)) {
41
+ throw new Error(`Refusing to send a Personal Access Token to untrusted destination '${requestUrl.origin}'`);
42
+ }
43
+ headers.set("Authorization", `Basic ${basicAuthValue}`);
44
+ if (input instanceof Request) {
45
+ return originalFetch(new Request(input, { ...init, headers }));
46
+ }
47
+ return originalFetch(input, { ...init, headers });
48
+ };
49
+ }
50
+ class OAuthAuthenticator {
51
+ static clientId = "0d50963b-7bb9-4fe7-94c7-a99af00b5136";
52
+ static defaultAuthority = "https://login.microsoftonline.com/common";
53
+ static zeroTenantId = "00000000-0000-0000-0000-000000000000";
54
+ accountId;
55
+ publicClientApp;
56
+ publicClientAppFallback;
57
+ constructor(tenantId) {
58
+ this.accountId = null;
59
+ let authority = OAuthAuthenticator.defaultAuthority;
60
+ if (tenantId && tenantId !== OAuthAuthenticator.zeroTenantId) {
61
+ authority = `https://login.microsoftonline.com/${tenantId}`;
62
+ logger.debug(`OAuthAuthenticator: Using tenant-specific authority for tenantId='${tenantId}'`);
63
+ }
64
+ else {
65
+ logger.debug(`OAuthAuthenticator: Using default common authority`);
66
+ }
67
+ this.publicClientApp = new PublicClientApplication({
68
+ auth: {
69
+ clientId: OAuthAuthenticator.clientId,
70
+ authority,
71
+ },
72
+ broker: {
73
+ nativeBrokerPlugin: new NativeBrokerPlugin(),
74
+ },
75
+ system: {
76
+ loggerOptions: {
77
+ loggerCallback: (level, message) => {
78
+ logger.debug(`MSALClient[${level}]: ${message}`);
79
+ },
80
+ },
81
+ },
82
+ });
83
+ this.publicClientAppFallback = new PublicClientApplication({
84
+ auth: {
85
+ clientId: OAuthAuthenticator.clientId,
86
+ authority,
87
+ },
88
+ });
89
+ logger.debug(`OAuthAuthenticator: Initialized with clientId='${OAuthAuthenticator.clientId}'`);
90
+ }
91
+ async getToken() {
92
+ let authResult = null;
93
+ if (this.accountId) {
94
+ logger.debug(`OAuthAuthenticator: Attempting silent token acquisition for cached account`);
95
+ try {
96
+ authResult = await this.publicClientApp.acquireTokenSilent({
97
+ scopes,
98
+ account: this.accountId,
99
+ });
100
+ logger.debug(`OAuthAuthenticator: Successfully acquired token silently`);
101
+ }
102
+ catch (error) {
103
+ logger.debug(`OAuthAuthenticator: Silent token acquisition failed: ${error instanceof Error ? error.message : String(error)}`);
104
+ authResult = null;
105
+ }
106
+ }
107
+ else {
108
+ logger.debug(`OAuthAuthenticator: No cached account available, interactive auth required`);
109
+ }
110
+ if (!authResult) {
111
+ logger.debug(`OAuthAuthenticator: Starting interactive token acquisition`);
112
+ try {
113
+ authResult = await this.publicClientApp.acquireTokenInteractive({
114
+ scopes,
115
+ openBrowser: async (url) => {
116
+ logger.debug(`OAuthAuthenticator: Opening browser for authentication with target URL: ${url}`);
117
+ open(url);
118
+ },
119
+ });
120
+ this.accountId = authResult.account;
121
+ logger.debug(`OAuthAuthenticator: Successfully acquired token interactively, account cached`);
122
+ }
123
+ catch (error) {
124
+ const msalErrorMessage = error.platformBrokerError ? JSON.stringify(error.platformBrokerError) : "";
125
+ logger.debug(`OAuthAuthenticator: Interactive token acquisition failed: ${error instanceof Error ? error.message + msalErrorMessage : String(error)}`);
126
+ authResult = null;
127
+ }
128
+ }
129
+ if (!authResult) {
130
+ logger.debug(`OAuthAuthenticator: Starting interactive token acquisition without broker`);
131
+ authResult = await this.publicClientAppFallback.acquireTokenInteractive({
132
+ scopes,
133
+ openBrowser: async (url) => {
134
+ logger.debug(`OAuthAuthenticator: Opening browser for authentication with target URL: ${url}`);
135
+ open(url);
136
+ },
137
+ });
138
+ logger.debug(`OAuthAuthenticator: Successfully acquired token interactively without broker`);
139
+ }
140
+ if (!authResult?.accessToken) {
141
+ logger.error(`OAuthAuthenticator: Authentication result contains no access token`);
142
+ throw new Error("Failed to obtain Azure DevOps OAuth token.");
143
+ }
144
+ logger.debug(`OAuthAuthenticator: Token obtained successfully`);
145
+ return authResult.accessToken;
146
+ }
147
+ }
148
+ function createAuthenticator(type, tenantId) {
149
+ logger.debug(`Creating authenticator of type '${type}' with tenantId='${tenantId ?? "undefined"}'`);
150
+ switch (type) {
151
+ case "pat":
152
+ logger.debug(`Authenticator: Using PAT authentication (PERSONAL_ACCESS_TOKEN)`);
153
+ return async () => {
154
+ logger.debug(`${type}: Reading token from PERSONAL_ACCESS_TOKEN environment variable`);
155
+ const rawPat = process.env["PERSONAL_ACCESS_TOKEN"];
156
+ if (!rawPat) {
157
+ logger.error(`${type}: PERSONAL_ACCESS_TOKEN environment variable is not set or empty`);
158
+ throw new Error("Environment variable 'PERSONAL_ACCESS_TOKEN' is not set or empty. Please set it to a valid Azure DevOps Personal Access Token.");
159
+ }
160
+ logger.debug(`${type}: Successfully retrieved PAT from environment variable`);
161
+ return rawPat;
162
+ };
163
+ case "envvar":
164
+ logger.debug(`Authenticator: Using environment variable authentication (ADO_MCP_AUTH_TOKEN)`);
165
+ // Read token from fixed environment variable
166
+ return async () => {
167
+ logger.debug(`${type}: Reading token from ADO_MCP_AUTH_TOKEN environment variable`);
168
+ const token = process.env["ADO_MCP_AUTH_TOKEN"];
169
+ if (!token) {
170
+ logger.error(`${type}: ADO_MCP_AUTH_TOKEN environment variable is not set or empty`);
171
+ throw new Error("Environment variable 'ADO_MCP_AUTH_TOKEN' is not set or empty. Please set it with a valid Azure DevOps Personal Access Token.");
172
+ }
173
+ logger.debug(`${type}: Successfully retrieved token from environment variable`);
174
+ return token;
175
+ };
176
+ case "azcli":
177
+ case "env":
178
+ if (type !== "env") {
179
+ logger.debug(`${type}: Setting AZURE_TOKEN_CREDENTIALS to 'dev' for development credential chain`);
180
+ process.env.AZURE_TOKEN_CREDENTIALS = "dev";
181
+ }
182
+ let credential = new DefaultAzureCredential(); // CodeQL [SM05138] resolved by explicitly setting AZURE_TOKEN_CREDENTIALS
183
+ if (tenantId) {
184
+ // Use Azure CLI credential if tenantId is provided for multi-tenant scenarios
185
+ const azureCliCredential = new AzureCliCredential({ tenantId });
186
+ credential = new ChainedTokenCredential(azureCliCredential, credential);
187
+ }
188
+ return async () => {
189
+ const result = await credential.getToken(scopes);
190
+ if (!result) {
191
+ logger.error(`${type}: Failed to obtain token - credential.getToken returned null/undefined`);
192
+ throw new Error("Failed to obtain Azure DevOps token. Ensure you have Azure CLI logged or use interactive type of authentication.");
193
+ }
194
+ logger.debug(`${type}: Successfully obtained Azure DevOps token`);
195
+ return result.token;
196
+ };
197
+ default:
198
+ logger.debug(`Authenticator: Using OAuth interactive authentication (default)`);
199
+ const authenticator = new OAuthAuthenticator(tenantId);
200
+ return () => {
201
+ return authenticator.getToken();
202
+ };
203
+ }
204
+ }
205
+ export { createAuthenticator, installPatFetchInterceptor };
package/dist/index.js ADDED
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ // Copyright (c) Microsoft Corporation.
3
+ // Licensed under the MIT License.
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { getBearerHandler, getPersonalAccessTokenHandler, WebApi } from "azure-devops-node-api";
7
+ import yargs from "yargs";
8
+ import { createAuthenticator, installPatFetchInterceptor } from "./auth.js";
9
+ import { logger } from "./logger.js";
10
+ import { getOrgTenant } from "./org-tenants.js";
11
+ //import { configurePrompts } from "./prompts.js";
12
+ import { configureAllTools } from "./tools.js";
13
+ import { UserAgentComposer } from "./useragent.js";
14
+ import { getCliArgs, resolveOrgUrl } from "./utils.js";
15
+ import { packageVersion } from "./version.js";
16
+ import { DomainsManager } from "./shared/domains.js";
17
+ function isGitHubCodespaceEnv() {
18
+ return process.env.CODESPACES === "true" && !!process.env.CODESPACE_NAME;
19
+ }
20
+ const defaultAuthenticationType = isGitHubCodespaceEnv() ? "azcli" : "interactive";
21
+ // Parse command line arguments using yargs
22
+ const argv = yargs(getCliArgs())
23
+ .scriptName("mcp-server-azuredevops")
24
+ .usage("Usage: $0 <organization> [options]")
25
+ .version(packageVersion)
26
+ .command("$0 <organization> [options]", "Azure DevOps MCP Server", (yargs) => {
27
+ yargs.positional("organization", {
28
+ describe: "Azure DevOps organization name (e.g. 'contoso'), or a full base URL for on-premises Azure DevOps Server / TFS (e.g. 'http://tfsserver:8080/tfs/DefaultCollection')",
29
+ type: "string",
30
+ demandOption: true,
31
+ });
32
+ })
33
+ .option("domains", {
34
+ alias: "d",
35
+ describe: "Domain(s) to enable: 'all' for everything, or specific domains like 'repositories builds work'. Defaults to 'all'.",
36
+ type: "string",
37
+ array: true,
38
+ default: "all",
39
+ })
40
+ .option("authentication", {
41
+ alias: "a",
42
+ describe: "Type of authentication to use",
43
+ type: "string",
44
+ choices: ["interactive", "azcli", "env", "envvar", "pat"],
45
+ default: defaultAuthenticationType,
46
+ })
47
+ .option("tenant", {
48
+ alias: "t",
49
+ describe: "Azure tenant ID (optional, applied when using 'interactive' and 'azcli' type of authentication)",
50
+ type: "string",
51
+ })
52
+ .help()
53
+ .parseSync();
54
+ const { orgUrl, cloudOrgName } = resolveOrgUrl(argv.organization);
55
+ // Preserved for the handful of tools that still call fixed Azure DevOps Services
56
+ // endpoints (e.g. code/wiki/work-item search) rather than the connected server's URL.
57
+ // Those remain cloud-only; see docs/TOOLSET.md and README for the on-prem limitation.
58
+ export const orgName = cloudOrgName ?? argv.organization;
59
+ const domainsManager = new DomainsManager(argv.domains);
60
+ export const enabledDomains = domainsManager.getEnabledDomains();
61
+ function getAzureDevOpsClient(getAzureDevOpsToken, userAgentComposer, authType) {
62
+ return async () => {
63
+ const accessToken = await getAzureDevOpsToken();
64
+ const authHandler = authType === "pat" ? getPersonalAccessTokenHandler(accessToken) : getBearerHandler(accessToken);
65
+ const connection = new WebApi(orgUrl, authHandler, undefined, {
66
+ productName: "AzureDevOps.MCP",
67
+ productVersion: packageVersion,
68
+ userAgent: userAgentComposer.userAgent,
69
+ });
70
+ return connection;
71
+ };
72
+ }
73
+ async function main() {
74
+ logger.info("Starting Azure DevOps MCP Server", {
75
+ organization: orgName,
76
+ organizationUrl: orgUrl,
77
+ authentication: argv.authentication,
78
+ tenant: argv.tenant,
79
+ domains: argv.domains,
80
+ enabledDomains: Array.from(enabledDomains),
81
+ version: packageVersion,
82
+ isCodespace: isGitHubCodespaceEnv(),
83
+ });
84
+ const server = new McpServer({
85
+ name: "Azure DevOps MCP Server",
86
+ version: packageVersion,
87
+ icons: [
88
+ {
89
+ src: "https://cdn.vsassets.io/content/icons/favicon.ico",
90
+ },
91
+ ],
92
+ });
93
+ const userAgentComposer = new UserAgentComposer(packageVersion);
94
+ server.server.oninitialized = () => {
95
+ userAgentComposer.appendMcpClientInfo(server.server.getClientVersion());
96
+ };
97
+ if (!cloudOrgName && !["pat", "envvar"].includes(argv.authentication)) {
98
+ logger.warn(`Authentication type '${argv.authentication}' was requested against a non-Azure DevOps Services URL ('${orgUrl}'). Azure DevOps Server (on-premises) generally only supports 'pat' (or 'envvar') authentication.`);
99
+ }
100
+ const tenantId = argv.tenant ?? (cloudOrgName ? await getOrgTenant(cloudOrgName) : undefined);
101
+ const authenticator = createAuthenticator(argv.authentication, tenantId);
102
+ if (argv.authentication === "pat") {
103
+ const rawPat = await authenticator();
104
+ installPatFetchInterceptor(rawPat, new URL(orgUrl).hostname);
105
+ logger.debug("PAT mode: global fetch interceptor installed to rewrite Bearer -> Basic auth headers");
106
+ }
107
+ // removing prompts until further notice
108
+ // configurePrompts(server);
109
+ configureAllTools(server, authenticator, getAzureDevOpsClient(authenticator, userAgentComposer, argv.authentication), () => userAgentComposer.userAgent, enabledDomains);
110
+ const transport = new StdioServerTransport();
111
+ await server.connect(transport);
112
+ }
113
+ main().catch((error) => {
114
+ logger.error("Fatal error in main():", error);
115
+ process.exit(1);
116
+ });
package/dist/logger.js ADDED
@@ -0,0 +1,34 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import winston from "winston";
4
+ import { setLogLevel } from "@azure/logger";
5
+ const logLevel = process.env.LOG_LEVEL?.toLowerCase();
6
+ if (logLevel && ["verbose", "debug", "info", "warning", "error"].includes(logLevel)) {
7
+ // Map Winston log levels to Azure log levels
8
+ const logLevelMap = {
9
+ verbose: "verbose",
10
+ debug: "info",
11
+ info: "info",
12
+ warning: "warning",
13
+ error: "error",
14
+ };
15
+ const azureLogLevel = logLevelMap[logLevel];
16
+ setLogLevel(azureLogLevel);
17
+ }
18
+ /**
19
+ * Logger utility for MCP server
20
+ *
21
+ * Since MCP servers use stdio transport for communication on stdout,
22
+ * we log to stderr to avoid interfering with the MCP protocol.
23
+ */
24
+ export const logger = winston.createLogger({
25
+ level: process.env.LOG_LEVEL || "info",
26
+ format: winston.format.combine(winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json()),
27
+ transports: [
28
+ new winston.transports.Stream({
29
+ stream: process.stderr,
30
+ }),
31
+ ],
32
+ // Prevent Winston from exiting on error
33
+ exitOnError: false,
34
+ });