Skip to content

Commit 635c1f7

Browse files
Adds tools to fetch user and team info (#31)
* Implement a wrapper to let the LLMs fetch information about the current user * Implement a wrapper to let the LLMs to fetch information about the teams members * Implement 2 new tools that leverage the newly introduced wrappers to get the current user and list team members * Switch to use organization get_profile, avoid duplication of functionalities * Improve CSV serialization error handling robustness
1 parent 0e59726 commit 635c1f7

File tree

3 files changed

+155
-0
lines changed

3 files changed

+155
-0
lines changed

src/azure/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ pub mod models;
55
pub mod organizations;
66
pub mod projects;
77
pub mod tags;
8+
pub mod teams;
89
pub mod work_items;

src/azure/teams.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use crate::azure::client::{AzureDevOpsClient, AzureError};
2+
use reqwest::Method;
3+
use serde::{Deserialize, Serialize};
4+
5+
#[derive(Debug, Serialize, Deserialize)]
6+
pub struct TeamMember {
7+
pub identity: TeamMemberIdentity,
8+
}
9+
10+
#[derive(Debug, Serialize, Deserialize)]
11+
pub struct TeamMemberIdentity {
12+
#[serde(rename = "displayName")]
13+
pub display_name: String,
14+
#[serde(rename = "uniqueName")]
15+
pub unique_name: String,
16+
pub id: String,
17+
}
18+
19+
#[derive(Debug, Deserialize)]
20+
struct TeamMembersResponse {
21+
value: Vec<TeamMember>,
22+
}
23+
24+
impl AzureDevOpsClient {
25+
pub async fn list_team_members(
26+
&self,
27+
organization: &str,
28+
project: &str,
29+
team_id: &str,
30+
) -> Result<Vec<TeamMember>, AzureError> {
31+
let path = format!(
32+
"projects/{}/teams/{}/members?api-version=7.1",
33+
project, team_id
34+
);
35+
let response: TeamMembersResponse = self
36+
.org_request(organization, Method::GET, &path, None::<&String>)
37+
.await?;
38+
39+
Ok(response.value)
40+
}
41+
}

src/mcp/server.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,23 @@ struct GetTeamIterationsArgs {
432432
team_id: String,
433433
}
434434

435+
#[derive(Deserialize, JsonSchema)]
436+
struct ListTeamMembersArgs {
437+
/// AzDO org name
438+
#[serde(deserialize_with = "deserialize_non_empty_string")]
439+
organization: String,
440+
/// AzDO project name
441+
#[serde(deserialize_with = "deserialize_non_empty_string")]
442+
project: String,
443+
/// Team ID or name
444+
team_id: String,
445+
}
446+
447+
#[derive(Deserialize, JsonSchema)]
448+
struct GetCurrentUserArgs {
449+
// No parameters needed
450+
}
451+
435452
#[derive(Deserialize, JsonSchema)]
436453
struct GetWorkItemArgs {
437454
/// AzDO org name
@@ -821,6 +838,102 @@ impl AzureMcpServer {
821838
)]))
822839
}
823840

841+
#[tool(description = "List team members")]
842+
async fn azdo_list_team_members(
843+
&self,
844+
args: Parameters<ListTeamMembersArgs>,
845+
) -> Result<CallToolResult, McpError> {
846+
log::info!("Tool invoked: azdo_list_team_members");
847+
let members = self
848+
.client
849+
.list_team_members(&args.0.organization, &args.0.project, &args.0.team_id)
850+
.await
851+
.map_err(|e| McpError {
852+
code: ErrorCode(-32000),
853+
message: e.to_string().into(),
854+
data: None,
855+
})?;
856+
857+
let mut wtr = csv::WriterBuilder::new()
858+
.has_headers(false)
859+
.from_writer(vec![]);
860+
861+
for member in members {
862+
wtr.write_record(&[member.identity.display_name, member.identity.unique_name])
863+
.map_err(|e| McpError {
864+
code: ErrorCode(-32000),
865+
message: format!("Failed to write CSV: {}", e).into(),
866+
data: None,
867+
})?;
868+
}
869+
870+
wtr.flush().map_err(|e| McpError {
871+
code: ErrorCode(-32000),
872+
message: format!("Failed to flush CSV: {}", e).into(),
873+
data: None,
874+
})?;
875+
876+
let csv_bytes = wtr.into_inner().map_err(|e| McpError {
877+
code: ErrorCode(-32000),
878+
message: format!("Failed to get CSV bytes: {}", e).into(),
879+
data: None,
880+
})?;
881+
882+
let data = String::from_utf8(csv_bytes).map_err(|e| McpError {
883+
code: ErrorCode(-32000),
884+
message: format!("Failed to convert CSV to string: {}", e).into(),
885+
data: None,
886+
})?;
887+
888+
Ok(CallToolResult::success(vec![Content::text(data)]))
889+
}
890+
891+
#[tool(description = "Get current user profile")]
892+
async fn azdo_get_current_user(
893+
&self,
894+
_args: Parameters<GetCurrentUserArgs>,
895+
) -> Result<CallToolResult, McpError> {
896+
log::info!("Tool invoked: azdo_get_current_user");
897+
let profile = organizations::get_profile(&self.client)
898+
.await
899+
.map_err(|e| McpError {
900+
code: ErrorCode(-32000),
901+
message: e.to_string().into(),
902+
data: None,
903+
})?;
904+
905+
let mut wtr = csv::WriterBuilder::new()
906+
.has_headers(false)
907+
.from_writer(vec![]);
908+
909+
wtr.write_record(&[profile.display_name, profile.email_address])
910+
.map_err(|e| McpError {
911+
code: ErrorCode(-32000),
912+
message: format!("Failed to write CSV: {}", e).into(),
913+
data: None,
914+
})?;
915+
916+
wtr.flush().map_err(|e| McpError {
917+
code: ErrorCode(-32000),
918+
message: format!("Failed to flush CSV: {}", e).into(),
919+
data: None,
920+
})?;
921+
922+
let csv_bytes = wtr.into_inner().map_err(|e| McpError {
923+
code: ErrorCode(-32000),
924+
message: format!("Failed to get CSV bytes: {}", e).into(),
925+
data: None,
926+
})?;
927+
928+
let data = String::from_utf8(csv_bytes).map_err(|e| McpError {
929+
code: ErrorCode(-32000),
930+
message: format!("Failed to convert CSV to string: {}", e).into(),
931+
data: None,
932+
})?;
933+
934+
Ok(CallToolResult::success(vec![Content::text(data)]))
935+
}
936+
824937
#[tool(description = "List AzDO organizations")]
825938
async fn azdo_list_organizations(
826939
&self,

0 commit comments

Comments
 (0)