Skip to content

Update ADO and Sonar MCP servers to use cloud URLs - #96

Merged
jamesgagan merged 3 commits into
mainfrom
agents/ado-sonar-cloud-url-update
Oct 24, 2026
Merged

jamesgagan merged 3 commits into
mainfrom
agents/ado-sonar-cloud-url-update

Conversation

@cbaerikebc

@cbaerikebc cbaerikebc commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

This pull request updates the ADO and Sonar MCP servers to use their respective cloud URLs as the default instead of placeholders. Key changes include:

  • ADO Configuration:

    • Opt-in cloud URL can be set to https://dev.azure.com.
    • ADO uses the existing ADO_DEFAULT_COLLECTION for both Cloud organizations and Server collections.
    • Implemented pagination for the list_projects method to handle cases where a Cloud organization may have more than 200 projects.
  • Sonar Configuration:

    • Opt-in cloud URL can be set to https://sonarcloud.io.
  • Documentation Updates:

    • Updated .github/copilot-instructions.md, README.md, and other documentation files to reflect the changes in configuration and usage.
  • Testing:

    • Added new tests for the updated functionality and ensured existing tests pass, with a focus on maintaining compatibility with on-prem ADO calls.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

SonarCloud project discovery needs organization scoping, and .NET scanning instructions need the correct organization argument.

2 open issues
What changed in this PR

Updates RAVEN’s ADO and Sonar MCP servers to default to cloud endpoints while preserving on-premises URL overrides.

Changes:

  • Adds cloud defaults and ADO organization selection.
  • Adds ADO project pagination and configuration tests.
  • Updates setup prompts and configuration documentation.
File Description
scripts/​setup-credentials.ps1 Updates Windows setup prompts.
scripts/​setup-credentials-mac.mjs Updates macOS setup prompts.
README.md Documents cloud configuration and scanning.
packages/​sonar-mcp/​src/​server.ts Defaults to SonarCloud.
packages/​sonar-mcp/​src/​__tests__/​server.test.ts Checks explicit URL overrides.
packages/​sonar-mcp/​src/​__tests__/​cloud-url.test.ts Tests the SonarCloud default.
packages/​ado-mcp/​src/​server.ts Adds cloud-aware organization handling.
packages/​ado-mcp/​src/​ado-client.ts Adds cloud detection and project pagination.
packages/​ado-mcp/​src/​__tests__/​server.test.ts Tests Cloud and Server configuration.
packages/​ado-mcp/​src/​__tests__/​ado-client.test.ts Tests organization URLs and pagination.
docs/​TOOL_INVENTORY.md Updates environment-variable requirements.
docs/​SYSTEM_DESIGN_AND_ARCHITECTURE.md Documents cloud destinations and defaults.
.github/​copilot-instructions.md Updates MCP configuration guidance.

Review effort: Balanced

Comment thread README.md Resolved
Comment thread packages/sonar-mcp/src/server.ts Resolved
- Refactor server.ts and sonar-client.ts for better URL management.
- Enhance test cases in cloud-url.test.ts and sonar-client.test.ts.
@cbaerikebc
cbaerikebc marked this pull request as ready for review October 20, 2026 02:14
@cbaerikebc

Copy link
Copy Markdown
Collaborator Author

@jamesgagan - over to you for review and merge

@jamesgagan jamesgagan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: #96

Head SHA: a029d73452d61367f1b35f462e989dcfa4c41d7e

Scope: Full review, all 16 changed files. Copilot's two inline comments on the first commit were checked and are resolved in code.

Tier: Deep, set by scripts/setup-credentials* and the two credential-sending clients · Round: 1 · Decoy: rejected 5/5


TL;DR

18 findings: 2 Important, 5 Nit, 11 Pre-existing. The URL building, pagination and tests are correct. The blocker is a design choice: a missing ADO_BASE_URL or SONARQUBE_URL now silently sends the stored credential to a public SaaS host instead of failing, and the security docs still promise the opposite.

About two hours to fix, mostly doc edits plus one guard and one test.

Required changes

  1. Make the Cloud destination explicit. Restore the is not set errors at packages/ado-mcp/src/server.ts:68 and packages/sonar-mcp/src/server.ts:20 and show the Cloud URLs uncommented in the README. If you keep the defaults instead, add a token-without-URL rejection to both setup scripts' validation blocks and make the server error name the resolved host.

  2. Add a test for ADO_PAT plus ADO_DEFAULT_COLLECTION with no ADO_BASE_URL, asserting whichever behaviour you choose in item 1. The current tests only assert the fallback as intended.

  3. Fix the compliance text that the defaults contradict: .github/copilot-instructions.md:142 and docs/SYSTEM_DESIGN_AND_ARCHITECTURE.md lines 628, 632 and 676. State that Azure DevOps Services and SonarQube Cloud are outside BC Gov infrastructure, are opt-in, and need a data-residency check. SonarQube Cloud has EU and US regions only.

Things that look scary but I verified are fine

  • Both Copilot comments are resolved. organization is required and forwarded on every page for Cloud, ignored on Server, with tests. The README and instructions document /o: for .NET and the scanner passes it through unchanged. The upstream .NET scanner source confirms it rejects /d:sonar.organization.

  • The $top rewrite. The old hand-built URL existed to keep $top unencoded. The new path keeps it literal on page one and page two, and tests pin the exact URLs.

  • Guard ordering. Every one of the 16 ADO tools calls getClient() before defaultCollection(), so the Cloud check always sees a live client. The new test covers the first-call case.

  • organization: undefined on Server. The Sonar request helper drops undefined params, so nothing is sent as the string "undefined".

  • No prior fix is reverted. Blame shows both removed checks date from the initial import with no recorded rationale. PR #65's security remediation did not touch endpoints. CI, including the inventory drift gate, is green.


Issue 1 — A missing URL now sends the credential to a third-party host [Important]

Code references:

  • [ADO server.ts L66-L75](
    function getClient(): AdoClient {
    if (!client) {
    const baseUrl = process.env["ADO_BASE_URL"] || "https://dev.azure.com";
    const pat = process.env["ADO_PAT"];
    if (!pat) throw new Error("ADO_PAT is not set. Add it to ~/.raven/.env.");
    const apiVersion = process.env["ADO_API_VERSION"] ?? "7.1";
    client = new AdoClient(baseUrl, pat, apiVersion);
    }
    return client;
    }
    )
  • [Sonar server.ts L18-L22](
    // ---------------------------------------------------------------------------
    function getServerUrl(): string {
    return process.env.SONARQUBE_URL || "https://sonarcloud.io";
    }
    function getToken(): string {
    )
- if (!baseUrl) throw new Error("ADO_BASE_URL is not set. Add it to ~/.raven/.env.");
+ const baseUrl = process.env["ADO_BASE_URL"] || "https://dev.azure.com";

- if (!url) throw new Error("SONARQUBE_URL is not set (see scripts/setup-credentials.ps1)");
+ return process.env.SONARQUBE_URL || "https://sonarcloud.io";

Steelman of the author's choice: Cloud-first is the PR's stated purpose. A token is still required, the setup prompts disclose the default, and an on-prem PAT is useless at dev.azure.com.

Why it still matters: This team runs both systems on-prem. A blank answer at the URL prompt, a typo in the variable name, a keychain migration that dropped one key, or a locked keychain now sends a live PAT, collection and project names, and write bodies to Microsoft, or a live Sonar token to SonarSource. sonar_run_scan puts the token and the cloud URL on the scanner command line. Error text never names the host, so the user cannot tell where the credential went. Every other server that needs a URL refuses to start without one, and both setup scripts already enforce URL-with-token for Jarvis. The section header still says "leave blank to skip", which no longer means skip.

Recommendation: See Required changes 1 and 2. If the defaults stay, add if (record.ADO_PAT && !record.ADO_BASE_URL?.trim()) and the Sonar twin to scripts/setup-credentials-mac.lib.mjs:48-56 and the matching block at scripts/setup-credentials.ps1:157-164.

Other sites with the same pattern: packages/github-mcp/src/server.ts:45 defaults to api.github.com, pre-existing, but it is paired with a mandatory repository allowlist and an HTTPS check. packages/sharepoint-mcp/src/sharepoint-client.ts:14 and packages/auth/src/cookie-cache.ts:20 fall back to non-resolving placeholder hosts, which is the repo's convention. Jenkins, Artifactory, RFC Buddy and Jarvis throw. Searched: _URL ... || / ?? across packages/*/src.

Verified: 5/5.

Issue 2 — The security and FOIPPA statements are now false [Important]

Code references:

  • [copilot-instructions.md L141-L143](
    Everything above is for repo navigation and PR review. **The sections below are for Copilot Chat in IDEs when this repo's MCP servers are connected** (clients pick these up via `.mcp.json`). RAVEN's MCP servers connect only to your configured BC Gov upstreams (Jira, Confluence, Bitbucket, app servers, and — when enabled — ADO, Sonar, Artifactory, RFC Buddy, and the `jarvis-mcp` remote proxy); they add no third-party data sinks beyond those. Artifactory direct downloads may additionally read from explicitly allowlisted HTTPS storage hosts without sending Artifactory credentials. See [`../docs/TOOL_INVENTORY.md`](../docs/TOOL_INVENTORY.md).
    )
  • [SYSTEM_DESIGN_AND_ARCHITECTURE.md L675-L677](
    | **FOIPPA s.30 (Protection of PI)** | PI scrubber anonymizes personal information before it reaches the AI. `RAVEN_SCRUB_PI=true` controls this globally. |
    | **FOIPPA s.30.1 (Storage in Canada)** | All enterprise data stays on BC Gov infrastructure. MCP servers run locally. GitHub Copilot Business does not retain data. |
    | **BC Gov ISP (Information Security Policy)** | Credentials stored with restricted permissions (`chmod 600`). Authentication via SiteMinder SSO or Basic Auth. TLS for all network communication. |
    )

RAVEN's MCP servers connect only to your configured BC Gov upstreams ... they add no third-party data sinks beyond those.

FOIPPA s.30.1 (Storage in Canada) | All enterprise data stays on BC Gov infrastructure.

Steelman of the author's choice: Line 628 was updated to name the cloud products "when configured", and a token is still an explicit act.

Why it still matters: Line 142 is an absolute with no such reading, and line 676 is the compliance table used in security walkthroughs. The PR added no residency caveat, no PIA or STRA note, and did not add these endpoints beside Jarvis in the data-residency callout. Line 632 says RAVEN "cannot phone home to an unconfigured host", which is exactly what the defaults do.

Recommendation: See Required change 3. If the defaults are dropped, this reduces to line 628 plus a residency note on 676.

Other sites with the same pattern: docs/SYSTEM_DESIGN_AND_ARCHITECTURE.md:249 and :255 ("fixed, configured endpoints"), pre-existing wording, weaker because the hosts are still fixed in code. docs/TOOL_INVENTORY.md:131-137 has the egress callout for Jarvis only.

Verified: 5/5.


Needs a human look

None. One Pre-existing item below had a 4/5 vote and is marked.

Lower-priority observations

Nits, all in this PR's code

  • Org in the base URL doubles the path. ADO_BASE_URL=https://dev.azure.com/myorg is classed Cloud by hostname, the error then tells the user to set the org, and the result is /myorg/myorg/{project}. Main's docblock documented the server/collection URL form, so this is a trap the PR's own message leads into. Reject a Cloud base URL with a path, or derive the org from it. [ado-client.ts#L44-L46](

    this.baseUrl = url;
    this.isCloud = new URL(url).hostname === "dev.azure.com";
    this.apiVersion = apiVersion;
    ) Verified 5/5.

  • list_projects error says "or supply collection" but the tool takes no arguments. The check at line 503 is also dead. README line 149 and the instructions string say any call can override the collection. [server.ts#L493-L509](

    server.tool(
    "list_projects",
    "List team projects in the configured Cloud organization or across collections on Azure DevOps Server.",
    {},
    { readOnlyHint: true },
    async () => {
    try {
    const ado = getClient();
    if (ado.isCloud) {
    const organization = defaultCollection();
    if (!organization) throw new Error("Set ADO_DEFAULT_COLLECTION to the Cloud organization name.");
    const data = await ado.listProjects(organization);
    const lines = data.value.map(
    (p) => `- **${p.name}** (${p.state})${p.description ? " — " + p.description : ""}`
    );
    return { content: [{ type: "text", text: lines.length ? lines.join("\n") : "No projects found." }] };
    }
    ) Verified 5/5.

  • Cloud detection is exact hostname equality. https://sonarqube.us is an official SonarQube Cloud region per SonarSource's scanner source, and it skips the organization guard and drops the parameter. Match the two known hosts. [sonar-client.ts#L69-L79](

    async listProjects(organization?: string): Promise<SonarProject[]> {
    const isCloud = new URL(this.baseUrl).hostname === "sonarcloud.io";
    const cloudOrganization = organization?.trim();
    if (isCloud && !cloudOrganization) {
    throw new Error("SonarQube Cloud project listing requires an organization argument.");
    }
    const projects: SonarProject[] = [];
    for (let page = 1; ; page++) {
    const result = await this.request<SonarProjectsPage>("api/components/search", {
    params: { qualifiers: "TRK", p: page, ps: 100, organization: isCloud ? cloudOrganization : undefined },
    });
    ) Verified 5/5.

  • list_release_pipelines on Cloud returns a raw 404. The docs say Server-only; a two-line isCloud guard would say so instead. [server.ts#L584-L600](

    server.tool(
    "list_release_pipelines",
    "List classic release pipelines on Azure DevOps Server (Cloud releases use a different host).",
    {
    project: z.string().optional().describe("Project name (uses ADO_DEFAULT_PROJECT if omitted)"),
    collection: z.string().optional().describe("Cloud organization or on-prem collection (uses ADO_DEFAULT_COLLECTION if omitted)"),
    },
    { readOnlyHint: true },
    async ({ project, collection }) => {
    try {
    const ado = getClient();
    const proj = defaultProject(project);
    const data = await ado.listReleasePipelines(proj, defaultCollection(collection));
    if (data.count === 0) return { content: [{ type: "text", text: `No release pipelines found in ${proj}.` }] };
    const lines = data.value.map(
    (p) => `- **#${p.id}** ${formatPipelineName(p.path, p.name)}`
    );
    ) Verified 5/5.

  • Stale on-prem wording left in edited files. README line 22, the collection JSDoc at ado-client.ts line 128, SYSTEM_DESIGN line 306, and .env.example lines 119-121 still say Server-only or omit the defaults. Verified 5/5.

Pre-existing, scanner execution surface (sonar-mcp)

  • sonar_run_scan runs npm test, dotnet build and dotnet test inside the AI-supplied projectDir with the full process environment, which holds every loaded credential. Untrusted repo test code can read them. [sonar-scanner.ts#L304-L306](

    return new Promise<RunCommandResult>((resolve) => {
    const child = spawn(bin, args, { cwd, env: process.env });
    let out = "", err = "";
    ) Verified 5/5.

  • The forbidden list is a six-entry denylist. It omits proxy, truststore, keystore, httpExtraHeaders, javaExePath, engineJarPath and javaOpts, all real upstream properties reachable through extraArgs. Two lenses read upstream as ignoring apiBaseUrl when sonar.host.url is set, so the host itself cannot be redirected. [server.ts#L197-L210](

    const forbidden = [
    "sonar.host.url",
    "sonar.token",
    "sonar.login",
    "sonar.password",
    "sonar.projectKey",
    "sonar.branch.name",
    ];
    const badArg = (extraArgs ?? []).find((a) => forbidden.some((k) => a.includes(k)));
    if (badArg) {
    throw new Error(`extraArgs may not set protected Sonar properties (received: ${badArg})`);
    }
    ) Verified 5/5.

  • The Sonar token is on the scanner command line in all three places. The Java scanner reads SONAR_TOKEN, but the child inherits SONARQUBE_TOKEN, so a fix must set SONAR_TOKEN in the spawn env explicitly. Not confirmed for the .NET scanner. [sonar-scanner.ts#L390-L393](

    `/k:${opts.projectKey}`,
    `/d:sonar.host.url=${opts.serverUrl}`,
    `/d:sonar.token=${opts.token}`,
    `/d:sonar.branch.name=${opts.branch}`,
    ) Verified 5/5.

  • -Dsonar.organization= becomes /d:sonar.organization= for .NET, which the scanner rejects. PR #67 translated name and version in code; organization is the same class, and extraArgs has no description. [sonar-scanner.ts#L408-L421](

    const projectNamePrefix = "-Dsonar.projectName=";
    const projectVersionPrefix = "-Dsonar.projectVersion=";
    for (const arg of opts.extraArgs ?? []) {
    if (arg.startsWith(projectNamePrefix)) {
    beginArgs.push("/n:" + arg.slice(projectNamePrefix.length));
    } else if (arg.startsWith(projectVersionPrefix)) {
    beginArgs.push("/v:" + arg.slice(projectVersionPrefix.length));
    } else if (arg.startsWith("-D")) {
    beginArgs.push("/d:" + arg.slice(2));
    } else {
    beginArgs.push(arg);
    }
    }
    ) Verified 5/5.

Pre-existing, ADO client hardening

  • No PI scrubbing anywhere in ado-mcp. Assignees, comment and PR authors, descriptions and error bodies reach the AI raw, while SYSTEM_DESIGN 7.4 says every server instantiates a PiScrubber. [server.ts#L90-L93](

    const safeErr = (err: unknown) =>
    err instanceof Error ? err.message : String(err);
    ) Verified 5/5.

  • http:// base URLs are accepted by both clients, so the Basic header goes in clear text. Jenkins, Artifactory and GitHub refuse non-HTTPS. The new new URL() calls are where the check fits. Your own PR #90 review listed this as a follow-up. [ado-client.ts#L40-L50](

    constructor(baseUrl: string, pat: string, apiVersion = "7.1", fetchFn?: typeof globalThis.fetch) {
    // Strip trailing slashes (string-based to avoid ReDoS-vulnerable regex)
    let url = baseUrl;
    while (url.endsWith("/")) url = url.slice(0, -1);
    this.baseUrl = url;
    this.isCloud = new URL(url).hostname === "dev.azure.com";
    this.apiVersion = apiVersion;
    // PAT auth: empty username, PAT as password
    this.authHeader = `Basic ${Buffer.from(`:${pat}`).toString("base64")}`;
    this.fetchFn = fetchFn ?? globalThis.fetch;
    }
    ) Verified 5/5.

  • Work-item comments appear to be a preview-only resource. Four verifiers found independent clients pinning 7.1-preview.x for comments and reports of VssInvalidPreviewVersionException on plain versions. If so, add_work_item_comment errors and get_work_item silently drops comments through its empty catch, on Server too. One live call settles it. [ado-client.ts#L205-L221](

    /** Get comments on a work item. */
    async getWorkItemComments(id: number, project: string, collection?: string): Promise<AdoWorkItemCommentsPage> {
    return this.request<AdoWorkItemCommentsPage>(
    `${this.projectPrefix(project, collection)}/_apis/wit/workitems/${id}/comments`
    );
    }
    /** Add a comment to a work item. */
    async addWorkItemComment(id: number, project: string, text: string, collection?: string): Promise<void> {
    await this.request(
    `${this.projectPrefix(project, collection)}/_apis/wit/workitems/${id}/comments`,
    {
    method: "POST",
    body: JSON.stringify({ text }),
    }
    );
    }
    ) Verified 4/5, one UNSURE.

  • .. passes through encodeURIComponent, so an AI-supplied collection or project can leave the configured virtual directory on the same host. Jenkins rejects dot segments. [ado-client.ts#L117-L123](

    /** Build the URL path prefix for a project, optionally scoped to a collection. */
    private projectPrefix(project: string, collection?: string): string {
    return collection
    ? `${encodeURIComponent(collection)}/${encodeURIComponent(project)}`
    : encodeURIComponent(project);
    }
    ) Verified 5/5.

  • The api-version comment "7.1 (supported by ADO Server 2020+)" is wrong; Microsoft's table caps Server 2020 at 6.0. listAllPages also has no page cap, so a server returning ever-changing tokens loops forever. [ado-client.ts#L29-L32](

    *
    * All requests target api-version 7.1 (supported by ADO Server 2020+).
    * Earlier versions (2019) can use 5.1 — override with ADO_API_VERSION env var.
    */
    ) Both verified 5/5.

Pre-existing, destination choice

The per-call collection on the four ADO write tools lets injected text pick any organization the PAT reaches, with no allowlist like GITHUB_REPOSITORY_ALLOWLIST. Bounded by PAT scope and the host client's write confirmation. The argument predates the PR; the PR makes the default host multi-tenant. Verified 5/5.

Two small setup-script items: the verify output shows four characters of every secret, and the PowerShell script writes the DPAPI file before tightening its ACL.


Findings I considered and dropped

  • Branch name agents/... is not a listed type. Squash-merged; not actionable.
  • new URL() in the constructor throws a bare "Invalid URL". Same failure fetch produced before; behaviour, not a defect.
  • The "(6 tools)" count removed from the SonarQube heading. The Jenkins heading has none either, and the inventory gate checks tool names only.
  • Legacy {org}.visualstudio.com hosts classed as Server. The Server path still works there.
  • Scanner validation skipped when NODE_ENV=test. Needs control of the server's environment, which already holds the secrets.
  • No fetch timeout or body-size cap in either client. Robustness only.
  • sonar.config in projectDir can set exclusions that hide findings. Scan integrity, outside this PR.
  • Host switching for vsrm.dev.azure.com. Documented as out of scope; the guard is the nit above.

Net recommendation

Fix the three Required changes first, then ship. The defaults are the only thing standing between this PR and a clean merge.

@jamesgagan jamesgagan mentioned this pull request Oct 23, 2026
5 tasks

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Cross-service routing, credential handling, and API compatibility changes need human validation of Cloud and Server behavior before approval.

1 open issue
2 resolved since last review

Review effort: Balanced

Comment thread packages/ado-mcp/src/server.ts Resolved
@cbaerikebc

Copy link
Copy Markdown
Collaborator Author

Hi @jamesgagan , thanks for the thorough review. This should now all be addressed.

@jamesgagan
jamesgagan merged commit 8433869 into main Oct 24, 2026
4 checks passed
@jamesgagan
jamesgagan deleted the agents/ado-sonar-cloud-url-update branch October 24, 2026 13:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants