Skip to content

Instantly share code, notes, and snippets.

@wullemsb
Created January 23, 2026 12:13
Show Gist options
  • Select an option

  • Save wullemsb/159235c780236d360ab1b2776146c38b to your computer and use it in GitHub Desktop.

Select an option

Save wullemsb/159235c780236d360ab1b2776146c38b to your computer and use it in GitHub Desktop.
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.TeamFoundation.Core.WebApi;
using Microsoft.VisualStudio.Services.Common;
using Microsoft.VisualStudio.Services.WebApi;
namespace VLM.SOFA.DevopsMCPServer;
/// <summary>
/// Scoped service that holds the project name and team name extracted from the route
/// Supports both /{projectName} and /{projectName}/{teamName} route patterns
/// </summary>
public class ProjectContext : IProjectContext
{
private readonly IHttpContextAccessor? _httpContextAccessor;
private string? _projectName;
private string? _teamName;
public ProjectContext(IHttpContextAccessor? httpContextAccessor = null)
{
_httpContextAccessor = httpContextAccessor;
}
/// <summary>
/// The project name from the route, or null if not provided
/// </summary>
public string? ProjectName
{
get
{
if (_httpContextAccessor?.HttpContext?.Request?.RouteValues?.TryGetValue("projectName", out var projectNameValue) == true)
{
_projectName = projectNameValue?.ToString();
}
return _projectName;
}
set => _projectName = value;
}
/// <summary>
/// The team name from the route, or null if not provided
/// </summary>
public string? TeamName
{
get
{
if (_httpContextAccessor?.HttpContext?.Request?.RouteValues?.TryGetValue("teamName", out var teamNameValue) == true)
{
_teamName = teamNameValue?.ToString();
}
return _teamName;
}
set => _teamName = value;
}
public string ResolveProjectName(AzureDevOpsConfigOptions options)
{
if (!string.IsNullOrWhiteSpace(ProjectName))
{
return ProjectName!;
}
return options.ProjectName ?? throw new ArgumentNullException(
nameof(options.ProjectName),
"ProjectName must be provided either in configuration or via route");
}
public async Task<string> ResolveTeamNameAsync(AzureDevOpsConfigOptions options)
{
// First, check if team name is provided in the route
if (!string.IsNullOrWhiteSpace(TeamName))
{
return TeamName!;
}
// Second, check if team name is provided in configuration
if (!string.IsNullOrWhiteSpace(options.TeamName))
{
return options.TeamName!;
}
// Third, fetch the first team from Azure DevOps
try
{
var projectName = ResolveProjectName(options);
var credentials = new VssBasicCredential(string.Empty, options.PersonalAccessToken);
using var connection = new VssConnection(new Uri(options.OrganizationUrl), credentials);
using var teamClient = connection.GetClient<TeamHttpClient>();
var teams = await teamClient.GetTeamsAsync(projectName);
if (teams != null && teams.Count > 0)
{
return teams[0].Name;
}
throw new InvalidOperationException(
$"No teams found for project '{projectName}'. TeamName must be provided in configuration, route, or at least one team must exist in the project.");
}
catch (Exception ex) when (ex is not InvalidOperationException)
{
throw new InvalidOperationException(
"Failed to retrieve teams from Azure DevOps. TeamName must be provided either in configuration or via route.",
ex);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment