#
tokens: 27371/50000 1/231 files (page 8/11)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 8 of 11. Use http://codebase.md/tuananh/hyper-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .cursor
│   └── rules
│       └── print-ctx-size.mdc
├── .dockerignore
├── .github
│   ├── renovate.json5
│   └── workflows
│       ├── ci.yml
│       ├── nightly.yml
│       └── release.yml
├── .gitignore
├── .gitmodules
├── .hadolint.yaml
├── .pre-commit-config.yaml
├── .windsurf
│   └── rules
│       ├── print-ctx-size.md
│       └── think.md
├── assets
│   ├── cursor-mcp-1.png
│   ├── cursor-mcp.png
│   ├── eval-py.jpg
│   └── logo.png
├── Cargo.lock
├── Cargo.toml
├── config.example.json
├── config.example.yaml
├── CREATING_PLUGINS.md
├── DEPLOYMENT.md
├── Dockerfile
├── examples
│   └── plugins
│       ├── v1
│       │   ├── arxiv
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── context7
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── crates-io
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── crypto-price
│       │   │   ├── Dockerfile
│       │   │   ├── go.mod
│       │   │   ├── go.sum
│       │   │   ├── main.go
│       │   │   ├── pdk.gen.go
│       │   │   └── README.md
│       │   ├── eval-py
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── fetch
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── fs
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── github
│       │   │   ├── .gitignore
│       │   │   ├── branches.go
│       │   │   ├── Dockerfile
│       │   │   ├── files.go
│       │   │   ├── gists.go
│       │   │   ├── go.mod
│       │   │   ├── go.sum
│       │   │   ├── issues.go
│       │   │   ├── main.go
│       │   │   ├── pdk.gen.go
│       │   │   ├── README.md
│       │   │   └── repo.go
│       │   ├── gitlab
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── gomodule
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── hash
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.lock
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── maven
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── meme-generator
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── generate_embedded.py
│       │   │   ├── README.md
│       │   │   ├── src
│       │   │   │   ├── embedded.rs
│       │   │   │   ├── lib.rs
│       │   │   │   └── pdk.rs
│       │   │   └── templates.json
│       │   ├── memory
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── myip
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.lock
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── qdrant
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       ├── pdk.rs
│       │   │       └── qdrant_client.rs
│       │   ├── qr-code
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.lock
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── serper
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── sqlite
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── think
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── time
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   ├── src
│       │   │   │   ├── lib.rs
│       │   │   │   └── pdk.rs
│       │   │   └── time.wasm
│       │   └── tool-list-changed
│       │       ├── .gitignore
│       │       ├── Cargo.toml
│       │       ├── Dockerfile
│       │       ├── README.md
│       │       ├── src
│       │       │   ├── lib.rs
│       │       │   └── pdk.rs
│       │       └── tool_list_changed.wasm
│       └── v2
│           └── rstime
│               ├── .cargo
│               │   └── config.toml
│               ├── .gitignore
│               ├── Cargo.toml
│               ├── Dockerfile
│               ├── README.md
│               ├── rstime.wasm
│               └── src
│                   ├── lib.rs
│                   └── pdk
│                       ├── exports.rs
│                       ├── imports.rs
│                       ├── mod.rs
│                       └── types.rs
├── iac
│   ├── .terraform.lock.hcl
│   ├── main.tf
│   ├── outputs.tf
│   └── variables.tf
├── justfile
├── LICENSE
├── README.md
├── RUNTIME_CONFIG.md
├── rust-toolchain.toml
├── server.json
├── SKIP_TOOLS_GUIDE.md
├── src
│   ├── cli.rs
│   ├── config.rs
│   ├── https_auth.rs
│   ├── logging.rs
│   ├── main.rs
│   ├── naming.rs
│   ├── plugin.rs
│   ├── service.rs
│   └── wasm
│       ├── http.rs
│       ├── mod.rs
│       ├── oci.rs
│       └── s3.rs
├── templates
│   └── plugins
│       ├── go
│       │   ├── .gitignore
│       │   ├── Dockerfile
│       │   ├── exports.go
│       │   ├── go.mod
│       │   ├── go.sum
│       │   ├── imports.go
│       │   ├── main.go
│       │   ├── README.md
│       │   └── types.go
│       ├── README.md
│       └── rust
│           ├── .cargo
│           │   └── config.toml
│           ├── .gitignore
│           ├── Cargo.toml
│           ├── Dockerfile
│           ├── README.md
│           └── src
│               ├── lib.rs
│               └── pdk
│                   ├── exports.rs
│                   ├── imports.rs
│                   ├── mod.rs
│                   └── types.rs
├── tests
│   └── fixtures
│       ├── config_with_auths.json
│       ├── config_with_auths.yaml
│       ├── documentation_example.json
│       ├── documentation_example.yaml
│       ├── invalid_auth_config.yaml
│       ├── invalid_plugin_name.yaml
│       ├── invalid_structure.yaml
│       ├── invalid_url.yaml
│       ├── keyring_auth_config.yaml
│       ├── skip_tools_examples.yaml
│       ├── unsupported_config.txt
│       ├── valid_config.json
│       └── valid_config.yaml
└── xtp-plugin-schema.json
```

# Files

--------------------------------------------------------------------------------
/examples/plugins/v1/gitlab/src/lib.rs:
--------------------------------------------------------------------------------

```rust
   1 | mod pdk;
   2 | 
   3 | use std::collections::BTreeMap;
   4 | 
   5 | use base64::prelude::*;
   6 | use extism_pdk::*;
   7 | use json::Value;
   8 | use pdk::types::{
   9 |     CallToolRequest, CallToolResult, Content, ContentType, ListToolsResult, ToolDescription,
  10 | };
  11 | use serde_json::json;
  12 | use termtree::Tree;
  13 | 
  14 | // Helper struct for deserializing GitLab API response
  15 | // https://docs.gitlab.com/api/repositories/#list-repository-tree
  16 | #[derive(serde::Deserialize)]
  17 | struct GitLabRepoEntry {
  18 |     id: String,
  19 |     name: String,
  20 |     r#type: String, // "tree" or "blob"
  21 |     path: String,
  22 |     mode: String,
  23 | }
  24 | 
  25 | // Helper struct for building the tree
  26 | #[derive(Debug)]
  27 | struct FileTreeNode {
  28 |     name: String,
  29 |     children: BTreeMap<String, FileTreeNode>,
  30 | }
  31 | 
  32 | impl FileTreeNode {
  33 |     fn new(name: &str) -> Self {
  34 |         FileTreeNode {
  35 |             name: name.to_string(),
  36 |             children: BTreeMap::new(),
  37 |         }
  38 |     }
  39 | 
  40 |     fn insert_path(&mut self, path_segments: &[&str]) {
  41 |         if path_segments.is_empty() {
  42 |             return;
  43 |         }
  44 |         let current_segment = path_segments[0];
  45 |         let node = self
  46 |             .children
  47 |             .entry(current_segment.to_string())
  48 |             .or_insert_with(|| FileTreeNode::new(current_segment));
  49 | 
  50 |         if path_segments.len() > 1 {
  51 |             node.insert_path(&path_segments[1..]);
  52 |         }
  53 |     }
  54 | }
  55 | 
  56 | // Renamed and modified function to convert FileTreeNode to termtree::Tree<String>
  57 | fn convert_file_tree_to_termtree(file_node: &FileTreeNode) -> Tree<String> {
  58 |     let mut tree_node = Tree::new(file_node.name.clone());
  59 |     for child_file_node in file_node.children.values() {
  60 |         // Iterate over sorted children
  61 |         tree_node.push(convert_file_tree_to_termtree(child_file_node));
  62 |     }
  63 |     tree_node
  64 | }
  65 | 
  66 | // New function to build and format the tree
  67 | fn build_and_format_tree_from_entries(
  68 |     entries: Vec<GitLabRepoEntry>,
  69 |     requested_path_opt: Option<&str>,
  70 |     project_id_str: &str,
  71 | ) -> Result<String, String> {
  72 |     if entries.is_empty() {
  73 |         return Ok("Repository tree is empty or path not found.".to_string());
  74 |     }
  75 | 
  76 |     let root_display_name = match requested_path_opt {
  77 |         Some(req_path) if !req_path.is_empty() => req_path
  78 |             .split('/')
  79 |             .next_back()
  80 |             .unwrap_or("root")
  81 |             .to_string(),
  82 |         _ => project_id_str
  83 |             .split('/')
  84 |             .next_back()
  85 |             .unwrap_or("root")
  86 |             .to_string(),
  87 |     };
  88 | 
  89 |     let mut root_node = FileTreeNode::new(&root_display_name);
  90 | 
  91 |     for entry in entries {
  92 |         let effective_path = match requested_path_opt {
  93 |             Some(base_path_val)
  94 |                 if !base_path_val.is_empty() && entry.path.starts_with(base_path_val) =>
  95 |             {
  96 |                 entry
  97 |                     .path
  98 |                     .strip_prefix(base_path_val)
  99 |                     .unwrap_or(&entry.path)
 100 |                     .trim_start_matches('/')
 101 |                     .to_string()
 102 |             }
 103 |             _ => entry.path.clone(),
 104 |         };
 105 | 
 106 |         if effective_path.is_empty() {
 107 |             continue;
 108 |         }
 109 | 
 110 |         let path_segments: Vec<&str> = effective_path
 111 |             .split('/')
 112 |             .filter(|s| !s.is_empty())
 113 |             .collect();
 114 |         if !path_segments.is_empty() {
 115 |             root_node.insert_path(&path_segments);
 116 |         }
 117 |     }
 118 | 
 119 |     let termtree_root = convert_file_tree_to_termtree(&root_node); // Use the new conversion function
 120 |     Ok(termtree_root.to_string())
 121 | }
 122 | 
 123 | fn get_gitlab_config() -> Result<(String, String), Error> {
 124 |     let token = config::get("GITLAB_TOKEN")?
 125 |         .ok_or_else(|| Error::msg("GITLAB_TOKEN configuration is required but not set"))?;
 126 | 
 127 |     let url = config::get("GITLAB_URL")?.unwrap_or_else(|| "https://gitlab.com/api/v4".to_string());
 128 | 
 129 |     Ok((token, url))
 130 | }
 131 | 
 132 | /// Helper function to check if an HTTP status code represents success (200-299)
 133 | fn is_success_status(status_code: u16) -> bool {
 134 |     (200..300).contains(&status_code)
 135 | }
 136 | 
 137 | fn urlencode_if_needed(input: &str) -> String {
 138 |     if input.contains("/") {
 139 |         urlencoding::encode(input).to_string()
 140 |     } else {
 141 |         input.to_string()
 142 |     }
 143 | }
 144 | 
 145 | pub(crate) fn call(input: CallToolRequest) -> Result<CallToolResult, Error> {
 146 |     info!("call: {:?}", input);
 147 |     match input.params.name.as_str() {
 148 |         // Issues
 149 |         "gl_create_issue" => create_issue(input),
 150 |         "gl_get_issue" => get_issue(input),
 151 |         "gl_update_issue" => update_issue(input),
 152 |         "gl_add_issue_comment" => add_issue_comment(input),
 153 |         "gl_list_issues" => gl_list_issues(input),
 154 | 
 155 |         // Files
 156 |         "gl_get_file_contents" => get_file_contents(input),
 157 |         "gl_create_or_update_file" => create_or_update_file(input),
 158 |         "gl_delete_file" => delete_file(input),
 159 | 
 160 |         // Branches
 161 |         "gl_create_branch" => create_branch(input),
 162 |         "gl_list_branches" => gl_list_branches(input),
 163 |         "gl_create_merge_request" => create_merge_request(input),
 164 |         "gl_update_merge_request" => update_merge_request(input),
 165 |         "gl_get_merge_request" => gl_get_merge_request(input),
 166 | 
 167 |         // Snippets (GitLab equivalent of Gists)
 168 |         "gl_create_snippet" => create_snippet(input),
 169 |         "gl_update_snippet" => update_snippet(input),
 170 |         "gl_get_snippet" => get_snippet(input),
 171 |         "gl_delete_snippet" => delete_snippet(input),
 172 | 
 173 |         // Repository tree
 174 |         "gl_get_repo_tree" => gl_get_repo_tree(input),
 175 | 
 176 |         // Repository members
 177 |         "gl_get_repo_members" => gl_get_repo_members(input),
 178 | 
 179 |         _ => Ok(CallToolResult {
 180 |             is_error: Some(true),
 181 |             content: vec![Content {
 182 |                 annotations: None,
 183 |                 text: Some(format!("Unknown operation: {}", input.params.name)),
 184 |                 mime_type: None,
 185 |                 r#type: ContentType::Text,
 186 |                 data: None,
 187 |             }],
 188 |         }),
 189 |     }
 190 | }
 191 | 
 192 | fn create_issue(input: CallToolRequest) -> Result<CallToolResult, Error> {
 193 |     let args = input.params.arguments.clone().unwrap_or_default();
 194 |     let (token, gitlab_url) = get_gitlab_config()?;
 195 | 
 196 |     if let (
 197 |         Some(Value::String(project_id)),
 198 |         Some(Value::String(title)),
 199 |         Some(Value::String(description)),
 200 |     ) = (
 201 |         args.get("project_id"),
 202 |         args.get("title"),
 203 |         args.get("description"),
 204 |     ) {
 205 |         let url = format!(
 206 |             "{}/projects/{}/issues",
 207 |             gitlab_url,
 208 |             urlencode_if_needed(project_id)
 209 |         );
 210 |         let mut body = json!({
 211 |             "title": title,
 212 |             "description": description,
 213 |         });
 214 | 
 215 |         // Add labels if provided
 216 |         if let Some(Value::String(labels)) = args.get("labels") {
 217 |             body.as_object_mut()
 218 |                 .unwrap()
 219 |                 .insert("labels".to_string(), Value::String(labels.clone()));
 220 |         }
 221 | 
 222 |         let mut headers = BTreeMap::new();
 223 |         headers.insert("PRIVATE-TOKEN".to_string(), token);
 224 |         headers.insert("Content-Type".to_string(), "application/json".to_string());
 225 |         headers.insert("User-Agent".to_string(), "hyper-mcp/0.1.0".to_string());
 226 | 
 227 |         let req = HttpRequest {
 228 |             url,
 229 |             headers,
 230 |             method: Some("POST".to_string()),
 231 |         };
 232 | 
 233 |         let res = http::request(&req, Some(&body.to_string()))?;
 234 | 
 235 |         if is_success_status(res.status_code()) {
 236 |             Ok(CallToolResult {
 237 |                 is_error: None,
 238 |                 content: vec![Content {
 239 |                     annotations: None,
 240 |                     text: Some(String::from_utf8_lossy(&res.body()).to_string()),
 241 |                     mime_type: Some("application/json".to_string()),
 242 |                     r#type: ContentType::Text,
 243 |                     data: None,
 244 |                 }],
 245 |             })
 246 |         } else {
 247 |             Ok(CallToolResult {
 248 |                 is_error: Some(true),
 249 |                 content: vec![Content {
 250 |                     annotations: None,
 251 |                     text: Some(format!("Failed to create issue: {}", res.status_code())),
 252 |                     mime_type: None,
 253 |                     r#type: ContentType::Text,
 254 |                     data: None,
 255 |                 }],
 256 |             })
 257 |         }
 258 |     } else {
 259 |         Ok(CallToolResult {
 260 |             is_error: Some(true),
 261 |             content: vec![Content {
 262 |                 annotations: None,
 263 |                 text: Some("Please provide project_id, title, and description".into()),
 264 |                 mime_type: None,
 265 |                 r#type: ContentType::Text,
 266 |                 data: None,
 267 |             }],
 268 |         })
 269 |     }
 270 | }
 271 | 
 272 | fn get_issue(input: CallToolRequest) -> Result<CallToolResult, Error> {
 273 |     let args = input.params.arguments.clone().unwrap_or_default();
 274 |     let (token, gitlab_url) = get_gitlab_config()?;
 275 | 
 276 |     if let (Some(Value::String(project_id)), Some(Value::String(issue_iid))) =
 277 |         (args.get("project_id"), args.get("issue_iid"))
 278 |     {
 279 |         let url = format!(
 280 |             "{}/projects/{}/issues/{}",
 281 |             gitlab_url,
 282 |             urlencode_if_needed(project_id),
 283 |             issue_iid
 284 |         );
 285 | 
 286 |         let mut headers = BTreeMap::new();
 287 |         headers.insert("PRIVATE-TOKEN".to_string(), token);
 288 |         headers.insert("User-Agent".to_string(), "hyper-mcp/0.1.0".to_string());
 289 | 
 290 |         let req = HttpRequest {
 291 |             url,
 292 |             headers,
 293 |             method: Some("GET".to_string()),
 294 |         };
 295 | 
 296 |         let res = http::request::<()>(&req, None)?;
 297 | 
 298 |         if is_success_status(res.status_code()) {
 299 |             Ok(CallToolResult {
 300 |                 is_error: None,
 301 |                 content: vec![Content {
 302 |                     annotations: None,
 303 |                     text: Some(String::from_utf8_lossy(&res.body()).to_string()),
 304 |                     mime_type: Some("application/json".to_string()),
 305 |                     r#type: ContentType::Text,
 306 |                     data: None,
 307 |                 }],
 308 |             })
 309 |         } else {
 310 |             Ok(CallToolResult {
 311 |                 is_error: Some(true),
 312 |                 content: vec![Content {
 313 |                     annotations: None,
 314 |                     text: Some(format!("Failed to get issue: {}", res.status_code())),
 315 |                     mime_type: None,
 316 |                     r#type: ContentType::Text,
 317 |                     data: None,
 318 |                 }],
 319 |             })
 320 |         }
 321 |     } else {
 322 |         Ok(CallToolResult {
 323 |             is_error: Some(true),
 324 |             content: vec![Content {
 325 |                 annotations: None,
 326 |                 text: Some("Please provide project_id and issue_iid".into()),
 327 |                 mime_type: None,
 328 |                 r#type: ContentType::Text,
 329 |                 data: None,
 330 |             }],
 331 |         })
 332 |     }
 333 | }
 334 | 
 335 | fn update_issue(input: CallToolRequest) -> Result<CallToolResult, Error> {
 336 |     let args = input.params.arguments.clone().unwrap_or_default();
 337 |     let (token, gitlab_url) = get_gitlab_config()?;
 338 | 
 339 |     if let (Some(Value::String(project_id)), Some(Value::String(issue_iid))) =
 340 |         (args.get("project_id"), args.get("issue_iid"))
 341 |     {
 342 |         let url = format!(
 343 |             "{}/projects/{}/issues/{}",
 344 |             gitlab_url,
 345 |             urlencode_if_needed(project_id),
 346 |             issue_iid
 347 |         );
 348 | 
 349 |         let mut body_map = serde_json::Map::new();
 350 |         if let Some(Value::String(title)) = args.get("title") {
 351 |             body_map.insert("title".to_string(), json!(title));
 352 |         }
 353 |         if let Some(Value::String(description)) = args.get("description") {
 354 |             body_map.insert("description".to_string(), json!(description));
 355 |         }
 356 |         if let Some(Value::String(add_labels)) = args.get("add_labels") {
 357 |             body_map.insert("add_labels".to_string(), json!(add_labels));
 358 |         }
 359 |         if let Some(Value::String(remove_labels)) = args.get("remove_labels") {
 360 |             body_map.insert("remove_labels".to_string(), json!(remove_labels));
 361 |         }
 362 |         if let Some(Value::String(due_date)) = args.get("due_date") {
 363 |             body_map.insert("due_date".to_string(), json!(due_date));
 364 |         }
 365 | 
 366 |         if body_map.is_empty() {
 367 |             return Ok(CallToolResult {
 368 |                 is_error: Some(true),
 369 |                 content: vec![Content {
 370 |                     annotations: None,
 371 |                     text: Some("Please provide at least one field to update (e.g., title, description, add_labels, remove_labels, due_date)".into()),
 372 |                     mime_type: None,
 373 |                     r#type: ContentType::Text,
 374 |                     data: None,
 375 |                 }],
 376 |             });
 377 |         }
 378 | 
 379 |         let body = Value::Object(body_map);
 380 | 
 381 |         let mut headers = BTreeMap::new();
 382 |         headers.insert("PRIVATE-TOKEN".to_string(), token);
 383 |         headers.insert("Content-Type".to_string(), "application/json".to_string());
 384 |         headers.insert("User-Agent".to_string(), "hyper-mcp/0.1.0".to_string());
 385 | 
 386 |         let req = HttpRequest {
 387 |             url,
 388 |             headers,
 389 |             method: Some("PUT".to_string()),
 390 |         };
 391 | 
 392 |         let res = http::request(&req, Some(&body.to_string()))?;
 393 | 
 394 |         if is_success_status(res.status_code()) {
 395 |             Ok(CallToolResult {
 396 |                 is_error: None,
 397 |                 content: vec![Content {
 398 |                     annotations: None,
 399 |                     text: Some(String::from_utf8_lossy(&res.body()).to_string()),
 400 |                     mime_type: Some("application/json".to_string()),
 401 |                     r#type: ContentType::Text,
 402 |                     data: None,
 403 |                 }],
 404 |             })
 405 |         } else {
 406 |             Ok(CallToolResult {
 407 |                 is_error: Some(true),
 408 |                 content: vec![Content {
 409 |                     annotations: None,
 410 |                     text: Some(format!(
 411 |                         "Failed to update issue: {} - Response: {}",
 412 |                         res.status_code(),
 413 |                         String::from_utf8_lossy(&res.body())
 414 |                     )),
 415 |                     mime_type: None,
 416 |                     r#type: ContentType::Text,
 417 |                     data: None,
 418 |                 }],
 419 |             })
 420 |         }
 421 |     } else {
 422 |         Ok(CallToolResult {
 423 |             is_error: Some(true),
 424 |             content: vec![Content {
 425 |                 annotations: None,
 426 |                 text: Some("Please provide project_id, issue_iid, and at least one field to update (title, description, add_labels, remove_labels, or due_date)".into()),
 427 |                 mime_type: None,
 428 |                 r#type: ContentType::Text,
 429 |                 data: None,
 430 |             }],
 431 |         })
 432 |     }
 433 | }
 434 | 
 435 | fn add_issue_comment(input: CallToolRequest) -> Result<CallToolResult, Error> {
 436 |     let args = input.params.arguments.clone().unwrap_or_default();
 437 |     let (token, gitlab_url) = get_gitlab_config()?;
 438 | 
 439 |     if let (
 440 |         Some(Value::String(project_id)),
 441 |         Some(Value::String(issue_iid)),
 442 |         Some(Value::String(comment)),
 443 |     ) = (
 444 |         args.get("project_id"),
 445 |         args.get("issue_iid"),
 446 |         args.get("comment"),
 447 |     ) {
 448 |         let url = format!(
 449 |             "{}/projects/{}/issues/{}/notes",
 450 |             gitlab_url,
 451 |             urlencode_if_needed(project_id),
 452 |             issue_iid
 453 |         );
 454 |         let body = json!({
 455 |             "body": comment,
 456 |         });
 457 | 
 458 |         let mut headers = BTreeMap::new();
 459 |         headers.insert("PRIVATE-TOKEN".to_string(), token);
 460 |         headers.insert("Content-Type".to_string(), "application/json".to_string());
 461 |         headers.insert("User-Agent".to_string(), "hyper-mcp/0.1.0".to_string());
 462 | 
 463 |         let req = HttpRequest {
 464 |             url,
 465 |             headers,
 466 |             method: Some("POST".to_string()),
 467 |         };
 468 | 
 469 |         let res = http::request(&req, Some(&body.to_string()))?;
 470 | 
 471 |         if is_success_status(res.status_code()) {
 472 |             Ok(CallToolResult {
 473 |                 is_error: None,
 474 |                 content: vec![Content {
 475 |                     annotations: None,
 476 |                     text: Some(String::from_utf8_lossy(&res.body()).to_string()),
 477 |                     mime_type: Some("application/json".to_string()),
 478 |                     r#type: ContentType::Text,
 479 |                     data: None,
 480 |                 }],
 481 |             })
 482 |         } else {
 483 |             Ok(CallToolResult {
 484 |                 is_error: Some(true),
 485 |                 content: vec![Content {
 486 |                     annotations: None,
 487 |                     text: Some(format!("Failed to add comment: {}", res.status_code())),
 488 |                     mime_type: None,
 489 |                     r#type: ContentType::Text,
 490 |                     data: None,
 491 |                 }],
 492 |             })
 493 |         }
 494 |     } else {
 495 |         Ok(CallToolResult {
 496 |             is_error: Some(true),
 497 |             content: vec![Content {
 498 |                 annotations: None,
 499 |                 text: Some("Please provide project_id, issue_iid, and comment".into()),
 500 |                 mime_type: None,
 501 |                 r#type: ContentType::Text,
 502 |                 data: None,
 503 |             }],
 504 |         })
 505 |     }
 506 | }
 507 | 
 508 | fn get_file_contents(input: CallToolRequest) -> Result<CallToolResult, Error> {
 509 |     let args = input.params.arguments.clone().unwrap_or_default();
 510 |     let (token, gitlab_url) = get_gitlab_config()?;
 511 | 
 512 |     if let (Some(Value::String(project_id)), Some(Value::String(file_path))) =
 513 |         (args.get("project_id"), args.get("file_path"))
 514 |     {
 515 |         let ref_name = args.get("ref").and_then(|v| v.as_str()).unwrap_or("HEAD");
 516 | 
 517 |         let url = format!(
 518 |             "{}/projects/{}/repository/files/{}?ref={}",
 519 |             gitlab_url,
 520 |             urlencode_if_needed(project_id),
 521 |             urlencode_if_needed(file_path),
 522 |             ref_name
 523 |         );
 524 | 
 525 |         let mut headers = BTreeMap::new();
 526 |         headers.insert("PRIVATE-TOKEN".to_string(), token);
 527 |         headers.insert("User-Agent".to_string(), "hyper-mcp/0.1.0".to_string());
 528 | 
 529 |         let req = HttpRequest {
 530 |             url: url.clone(),
 531 |             headers,
 532 |             method: Some("GET".to_string()),
 533 |         };
 534 | 
 535 |         let res = http::request::<()>(&req, None)?;
 536 | 
 537 |         if is_success_status(res.status_code()) {
 538 |             // Parse the response to get the file content from the "content" field
 539 |             if let Ok(json) = serde_json::from_slice::<Value>(&res.body()) {
 540 |                 if let Some(content) = json.get("content").and_then(|v| v.as_str()) {
 541 |                     // Decode base64 content
 542 |                     match BASE64_STANDARD.decode(content.as_bytes()) {
 543 |                         Ok(decoded_bytes) => {
 544 |                             if let Ok(decoded_content) = String::from_utf8(decoded_bytes) {
 545 |                                 return Ok(CallToolResult {
 546 |                                     is_error: None,
 547 |                                     content: vec![Content {
 548 |                                         annotations: None,
 549 |                                         text: Some(decoded_content),
 550 |                                         mime_type: Some("text/plain".to_string()),
 551 |                                         r#type: ContentType::Text,
 552 |                                         data: None,
 553 |                                     }],
 554 |                                 });
 555 |                             }
 556 |                         }
 557 |                         Err(e) => {
 558 |                             return Ok(CallToolResult {
 559 |                                 is_error: Some(true),
 560 |                                 content: vec![Content {
 561 |                                     annotations: None,
 562 |                                     text: Some(format!("Failed to decode base64 content: {}", e)),
 563 |                                     mime_type: None,
 564 |                                     r#type: ContentType::Text,
 565 |                                     data: None,
 566 |                                 }],
 567 |                             });
 568 |                         }
 569 |                     }
 570 |                 }
 571 |             }
 572 | 
 573 |             Ok(CallToolResult {
 574 |                 is_error: Some(true),
 575 |                 content: vec![Content {
 576 |                     annotations: None,
 577 |                     text: Some("Failed to parse file contents from response".into()),
 578 |                     mime_type: None,
 579 |                     r#type: ContentType::Text,
 580 |                     data: None,
 581 |                 }],
 582 |             })
 583 |         } else {
 584 |             Ok(CallToolResult {
 585 |                 is_error: Some(true),
 586 |                 content: vec![Content {
 587 |                     annotations: None,
 588 |                     text: Some(format!(
 589 |                         "Failed to get file contents: {} {}",
 590 |                         url.clone(),
 591 |                         res.status_code()
 592 |                     )),
 593 |                     mime_type: None,
 594 |                     r#type: ContentType::Text,
 595 |                     data: None,
 596 |                 }],
 597 |             })
 598 |         }
 599 |     } else {
 600 |         Ok(CallToolResult {
 601 |             is_error: Some(true),
 602 |             content: vec![Content {
 603 |                 annotations: None,
 604 |                 text: Some("Please provide project_id and file_path".into()),
 605 |                 mime_type: None,
 606 |                 r#type: ContentType::Text,
 607 |                 data: None,
 608 |             }],
 609 |         })
 610 |     }
 611 | }
 612 | 
 613 | fn delete_file(input: CallToolRequest) -> Result<CallToolResult, Error> {
 614 |     let args = input.params.arguments.clone().unwrap_or_default();
 615 |     let (token, gitlab_url) = get_gitlab_config()?;
 616 | 
 617 |     if let (
 618 |         Some(Value::String(project_id)),
 619 |         Some(Value::String(file_path)),
 620 |         Some(Value::String(branch)),
 621 |     ) = (
 622 |         args.get("project_id"),
 623 |         args.get("file_path"),
 624 |         args.get("branch"),
 625 |     ) {
 626 |         let commit_message = args
 627 |             .get("commit_message")
 628 |             .and_then(|v| v.as_str())
 629 |             .unwrap_or("Delete file via API")
 630 |             .to_string();
 631 | 
 632 |         let url = format!(
 633 |             "{}/projects/{}/repository/files/{}",
 634 |             gitlab_url,
 635 |             urlencode_if_needed(project_id),
 636 |             urlencode_if_needed(file_path)
 637 |         );
 638 | 
 639 |         let mut headers = BTreeMap::new();
 640 |         headers.insert("PRIVATE-TOKEN".to_string(), token);
 641 |         headers.insert("Content-Type".to_string(), "application/json".to_string());
 642 |         headers.insert("User-Agent".to_string(), "hyper-mcp/0.1.0".to_string());
 643 | 
 644 |         let mut body_map = serde_json::Map::new();
 645 |         body_map.insert("branch".to_string(), json!(branch));
 646 |         body_map.insert("commit_message".to_string(), json!(commit_message));
 647 |         if let Some(Value::String(author_email)) = args.get("author_email") {
 648 |             body_map.insert("author_email".to_string(), json!(author_email));
 649 |         }
 650 |         if let Some(Value::String(author_name)) = args.get("author_name") {
 651 |             body_map.insert("author_name".to_string(), json!(author_name));
 652 |         }
 653 |         let body = Value::Object(body_map);
 654 | 
 655 |         let req = HttpRequest {
 656 |             url,
 657 |             headers,
 658 |             method: Some("DELETE".to_string()),
 659 |         };
 660 | 
 661 |         let res = http::request(&req, Some(&body.to_string()))?;
 662 | 
 663 |         if is_success_status(res.status_code()) {
 664 |             Ok(CallToolResult {
 665 |                 is_error: None,
 666 |                 content: vec![Content {
 667 |                     annotations: None,
 668 |                     text: Some(String::from_utf8_lossy(&res.body()).to_string()),
 669 |                     mime_type: Some("application/json".to_string()),
 670 |                     r#type: ContentType::Text,
 671 |                     data: None,
 672 |                 }],
 673 |             })
 674 |         } else {
 675 |             Ok(CallToolResult {
 676 |                 is_error: Some(true),
 677 |                 content: vec![Content {
 678 |                     annotations: None,
 679 |                     text: Some(format!(
 680 |                         "Failed to delete file (status {}): {}",
 681 |                         res.status_code(),
 682 |                         String::from_utf8_lossy(&res.body())
 683 |                     )),
 684 |                     mime_type: None,
 685 |                     r#type: ContentType::Text,
 686 |                     data: None,
 687 |                 }],
 688 |             })
 689 |         }
 690 |     } else {
 691 |         Ok(CallToolResult {
 692 |             is_error: Some(true),
 693 |             content: vec![Content {
 694 |                 annotations: None,
 695 |                 text: Some("Please provide project_id, file_path, and branch".into()),
 696 |                 mime_type: None,
 697 |                 r#type: ContentType::Text,
 698 |                 data: None,
 699 |             }],
 700 |         })
 701 |     }
 702 | }
 703 | 
 704 | fn create_or_update_file(input: CallToolRequest) -> Result<CallToolResult, Error> {
 705 |     let args = input.params.arguments.clone().unwrap_or_default();
 706 |     let (token, gitlab_url) = get_gitlab_config()?;
 707 | 
 708 |     if let (
 709 |         Some(Value::String(project_id)),
 710 |         Some(Value::String(file_path)),
 711 |         Some(Value::String(content)),
 712 |         Some(Value::String(branch)),
 713 |     ) = (
 714 |         args.get("project_id"),
 715 |         args.get("file_path"),
 716 |         args.get("content"),
 717 |         args.get("branch"),
 718 |     ) {
 719 |         let commit_message = args
 720 |             .get("commit_message")
 721 |             .and_then(|v| v.as_str())
 722 |             .unwrap_or("Update file via API")
 723 |             .to_string();
 724 | 
 725 |         // URL for checking file existence. Note: GitLab GET file API needs ref in query.
 726 |         let check_file_url = format!(
 727 |             "{}/projects/{}/repository/files/{}?ref={}",
 728 |             gitlab_url,
 729 |             urlencode_if_needed(project_id),
 730 |             urlencode_if_needed(file_path),
 731 |             branch
 732 |         );
 733 | 
 734 |         let mut headers_check = BTreeMap::new();
 735 |         headers_check.insert("PRIVATE-TOKEN".to_string(), token.clone());
 736 |         headers_check.insert("User-Agent".to_string(), "hyper-mcp/0.1.0".to_string());
 737 | 
 738 |         let check_req = HttpRequest {
 739 |             url: check_file_url,
 740 |             headers: headers_check,
 741 |             method: Some("GET".to_string()),
 742 |         };
 743 | 
 744 |         let check_res = http::request::<()>(&check_req, None)?;
 745 | 
 746 |         let http_method = match check_res.status_code() {
 747 |             200 => "PUT",  // File exists, so update
 748 |             404 => "POST", // File does not exist, so create
 749 |             _ => {
 750 |                 return Ok(CallToolResult {
 751 |                     is_error: Some(true),
 752 |                     content: vec![Content {
 753 |                         annotations: None,
 754 |                         text: Some(format!(
 755 |                             "Failed to check file existence (status {} on GET {}): {}",
 756 |                             check_res.status_code(),
 757 |                             check_req.url,
 758 |                             String::from_utf8_lossy(&check_res.body())
 759 |                         )),
 760 |                         mime_type: None,
 761 |                         r#type: ContentType::Text,
 762 |                         data: None,
 763 |                     }],
 764 |                 });
 765 |             }
 766 |         };
 767 | 
 768 |         // URL for POST/PUT operations (does not have ref in query string, ref is in body via 'branch' parameter)
 769 |         // Ensure file_path is URL encoded for this URL as well.
 770 |         let operation_url = format!(
 771 |             "{}/projects/{}/repository/files/{}",
 772 |             gitlab_url,
 773 |             urlencode_if_needed(project_id),
 774 |             urlencode_if_needed(file_path)
 775 |         );
 776 | 
 777 |         let mut body_map = serde_json::Map::new();
 778 |         body_map.insert("branch".to_string(), json!(branch));
 779 |         body_map.insert("content".to_string(), json!(content));
 780 |         body_map.insert("commit_message".to_string(), json!(commit_message));
 781 | 
 782 |         if let Some(Value::String(author_email)) = args.get("author_email") {
 783 |             body_map.insert("author_email".to_string(), json!(author_email));
 784 |         }
 785 |         if let Some(Value::String(author_name)) = args.get("author_name") {
 786 |             body_map.insert("author_name".to_string(), json!(author_name));
 787 |         }
 788 |         // Note: For 'POST' (create), 'encoding' can be 'base64'.
 789 |         // GitLab API often expects content to be base64 encoded for new files if not plain text.
 790 |         // For simplicity, we assume content is plain text, and GitLab handles it.
 791 |         // If issues arise with binary or special characters, 'content' might need explicit base64 encoding
 792 |         // and adding "encoding": "base64" to body_map.
 793 | 
 794 |         let body = Value::Object(body_map);
 795 | 
 796 |         let mut headers_op = BTreeMap::new();
 797 |         headers_op.insert("PRIVATE-TOKEN".to_string(), token);
 798 |         headers_op.insert("Content-Type".to_string(), "application/json".to_string());
 799 |         headers_op.insert("User-Agent".to_string(), "hyper-mcp/0.1.0".to_string());
 800 | 
 801 |         let req = HttpRequest {
 802 |             url: operation_url.clone(),
 803 |             headers: headers_op,
 804 |             method: Some(http_method.to_string()),
 805 |         };
 806 | 
 807 |         let res = http::request(&req, Some(&body.to_string()))?;
 808 | 
 809 |         if is_success_status(res.status_code()) {
 810 |             Ok(CallToolResult {
 811 |                 is_error: None,
 812 |                 content: vec![Content {
 813 |                     annotations: None,
 814 |                     text: Some(String::from_utf8_lossy(&res.body()).to_string()),
 815 |                     mime_type: Some("application/json".to_string()),
 816 |                     r#type: ContentType::Text,
 817 |                     data: None,
 818 |                 }],
 819 |             })
 820 |         } else {
 821 |             Ok(CallToolResult {
 822 |                 is_error: Some(true),
 823 |                 content: vec![Content {
 824 |                     annotations: None,
 825 |                     text: Some(format!(
 826 |                         "Failed to {} file (method {}, status {} on {}): Response: {}",
 827 |                         if http_method == "POST" {
 828 |                             "create"
 829 |                         } else {
 830 |                             "update"
 831 |                         },
 832 |                         http_method,
 833 |                         res.status_code(),
 834 |                         req.url,
 835 |                         String::from_utf8_lossy(&res.body())
 836 |                     )),
 837 |                     mime_type: None,
 838 |                     r#type: ContentType::Text,
 839 |                     data: None,
 840 |                 }],
 841 |             })
 842 |         }
 843 |     } else {
 844 |         Ok(CallToolResult {
 845 |             is_error: Some(true),
 846 |             content: vec![Content {
 847 |                 annotations: None,
 848 |                 text: Some("Please provide project_id, file_path, content, and branch".into()),
 849 |                 mime_type: None,
 850 |                 r#type: ContentType::Text,
 851 |                 data: None,
 852 |             }],
 853 |         })
 854 |     }
 855 | }
 856 | 
 857 | fn create_branch(input: CallToolRequest) -> Result<CallToolResult, Error> {
 858 |     let args = input.params.arguments.clone().unwrap_or_default();
 859 |     let (token, gitlab_url) = get_gitlab_config()?;
 860 | 
 861 |     if let (
 862 |         Some(Value::String(project_id)),
 863 |         Some(Value::String(branch_name)),
 864 |         Some(Value::String(ref_name)),
 865 |     ) = (
 866 |         args.get("project_id"),
 867 |         args.get("branch_name"),
 868 |         args.get("ref"),
 869 |     ) {
 870 |         let url = format!(
 871 |             "{}/projects/{}/repository/branches",
 872 |             gitlab_url,
 873 |             urlencode_if_needed(project_id)
 874 |         );
 875 |         let body = json!({
 876 |             "branch": branch_name,
 877 |             "ref": ref_name
 878 |         });
 879 | 
 880 |         let mut headers = BTreeMap::new();
 881 |         headers.insert("PRIVATE-TOKEN".to_string(), token);
 882 |         headers.insert("Content-Type".to_string(), "application/json".to_string());
 883 |         headers.insert("User-Agent".to_string(), "hyper-mcp/0.1.0".to_string());
 884 | 
 885 |         let req = HttpRequest {
 886 |             url,
 887 |             headers,
 888 |             method: Some("POST".to_string()),
 889 |         };
 890 | 
 891 |         let res = http::request(&req, Some(&body.to_string()))?;
 892 | 
 893 |         if is_success_status(res.status_code()) {
 894 |             Ok(CallToolResult {
 895 |                 is_error: None,
 896 |                 content: vec![Content {
 897 |                     annotations: None,
 898 |                     text: Some(String::from_utf8_lossy(&res.body()).to_string()),
 899 |                     mime_type: Some("application/json".to_string()),
 900 |                     r#type: ContentType::Text,
 901 |                     data: None,
 902 |                 }],
 903 |             })
 904 |         } else {
 905 |             Ok(CallToolResult {
 906 |                 is_error: Some(true),
 907 |                 content: vec![Content {
 908 |                     annotations: None,
 909 |                     text: Some(format!("Failed to create branch: {}", res.status_code())),
 910 |                     mime_type: None,
 911 |                     r#type: ContentType::Text,
 912 |                     data: None,
 913 |                 }],
 914 |             })
 915 |         }
 916 |     } else {
 917 |         Ok(CallToolResult {
 918 |             is_error: Some(true),
 919 |             content: vec![Content {
 920 |                 annotations: None,
 921 |                 text: Some("Please provide project_id, branch_name, and ref".into()),
 922 |                 mime_type: None,
 923 |                 r#type: ContentType::Text,
 924 |                 data: None,
 925 |             }],
 926 |         })
 927 |     }
 928 | }
 929 | 
 930 | fn create_merge_request(input: CallToolRequest) -> Result<CallToolResult, Error> {
 931 |     let args = input.params.arguments.clone().unwrap_or_default();
 932 |     let (token, gitlab_url) = get_gitlab_config()?;
 933 | 
 934 |     if let (
 935 |         Some(Value::String(project_id)),
 936 |         Some(Value::String(source_branch)),
 937 |         Some(Value::String(target_branch)),
 938 |     ) = (
 939 |         args.get("project_id"),
 940 |         args.get("source_branch"),
 941 |         args.get("target_branch"),
 942 |     ) {
 943 |         let url = format!(
 944 |             "{}/projects/{}/merge_requests",
 945 |             gitlab_url,
 946 |             urlencode_if_needed(project_id)
 947 |         );
 948 | 
 949 |         // Use provided title if present, otherwise use default format
 950 |         let title = args
 951 |             .get("title")
 952 |             .and_then(|t| t.as_str())
 953 |             .map(|t| t.to_string())
 954 |             .unwrap_or_else(|| format!("Merge {} into {}", source_branch, target_branch));
 955 | 
 956 |         let body = json!({
 957 |             "source_branch": source_branch,
 958 |             "target_branch": target_branch,
 959 |             "title": title,
 960 |         });
 961 | 
 962 |         let mut headers = BTreeMap::new();
 963 |         headers.insert("PRIVATE-TOKEN".to_string(), token);
 964 |         headers.insert("Content-Type".to_string(), "application/json".to_string());
 965 |         headers.insert("User-Agent".to_string(), "hyper-mcp/0.1.0".to_string());
 966 | 
 967 |         let req = HttpRequest {
 968 |             url,
 969 |             headers,
 970 |             method: Some("POST".to_string()),
 971 |         };
 972 | 
 973 |         let res = http::request(&req, Some(&body.to_string()))?;
 974 | 
 975 |         if is_success_status(res.status_code()) {
 976 |             Ok(CallToolResult {
 977 |                 is_error: None,
 978 |                 content: vec![Content {
 979 |                     annotations: None,
 980 |                     text: Some(String::from_utf8_lossy(&res.body()).to_string()),
 981 |                     mime_type: Some("application/json".to_string()),
 982 |                     r#type: ContentType::Text,
 983 |                     data: None,
 984 |                 }],
 985 |             })
 986 |         } else {
 987 |             Ok(CallToolResult {
 988 |                 is_error: Some(true),
 989 |                 content: vec![Content {
 990 |                     annotations: None,
 991 |                     text: Some(format!(
 992 |                         "Failed to create merge request: {}",
 993 |                         res.status_code()
 994 |                     )),
 995 |                     mime_type: None,
 996 |                     r#type: ContentType::Text,
 997 |                     data: None,
 998 |                 }],
 999 |             })
1000 |         }
1001 |     } else {
1002 |         Ok(CallToolResult {
1003 |             is_error: Some(true),
1004 |             content: vec![Content {
1005 |                 annotations: None,
1006 |                 text: Some("Please provide project_id, source_branch, and target_branch".into()),
1007 |                 mime_type: None,
1008 |                 r#type: ContentType::Text,
1009 |                 data: None,
1010 |             }],
1011 |         })
1012 |     }
1013 | }
1014 | 
1015 | fn update_merge_request(input: CallToolRequest) -> Result<CallToolResult, Error> {
1016 |     let args = input.params.arguments.clone().unwrap_or_default();
1017 |     let (token, gitlab_url) = get_gitlab_config()?;
1018 | 
1019 |     if let (
1020 |         Some(Value::String(project_id)),
1021 |         Some(Value::String(merge_request_iid)),
1022 |         Some(Value::String(title)),
1023 |         Some(Value::String(description)),
1024 |     ) = (
1025 |         args.get("project_id"),
1026 |         args.get("merge_request_iid"),
1027 |         args.get("title"),
1028 |         args.get("description"),
1029 |     ) {
1030 |         let url = format!(
1031 |             "{}/projects/{}/merge_requests/{}",
1032 |             gitlab_url,
1033 |             urlencode_if_needed(project_id),
1034 |             merge_request_iid
1035 |         );
1036 | 
1037 |         let mut body_map = serde_json::Map::new();
1038 |         body_map.insert("title".to_string(), json!(title));
1039 |         body_map.insert("description".to_string(), json!(description));
1040 | 
1041 |         let body = Value::Object(body_map);
1042 | 
1043 |         let mut headers = BTreeMap::new();
1044 |         headers.insert("PRIVATE-TOKEN".to_string(), token);
1045 |         headers.insert("Content-Type".to_string(), "application/json".to_string());
1046 |         headers.insert("User-Agent".to_string(), "hyper-mcp/0.1.0".to_string());
1047 | 
1048 |         let req = HttpRequest {
1049 |             url,
1050 |             headers,
1051 |             method: Some("PUT".to_string()),
1052 |         };
1053 | 
1054 |         let res = http::request(&req, Some(&body.to_string()))?;
1055 | 
1056 |         if is_success_status(res.status_code()) {
1057 |             Ok(CallToolResult {
1058 |                 is_error: None,
1059 |                 content: vec![Content {
1060 |                     annotations: None,
1061 |                     text: Some(String::from_utf8_lossy(&res.body()).to_string()),
1062 |                     mime_type: Some("application/json".to_string()),
1063 |                     r#type: ContentType::Text,
1064 |                     data: None,
1065 |                 }],
1066 |             })
1067 |         } else {
1068 |             Ok(CallToolResult {
1069 |                 is_error: Some(true),
1070 |                 content: vec![Content {
1071 |                     annotations: None,
1072 |                     text: Some(format!(
1073 |                         "Failed to update merge request: {} - Response: {}",
1074 |                         res.status_code(),
1075 |                         String::from_utf8_lossy(&res.body())
1076 |                     )),
1077 |                     mime_type: None,
1078 |                     r#type: ContentType::Text,
1079 |                     data: None,
1080 |                 }],
1081 |             })
1082 |         }
1083 |     } else {
1084 |         Ok(CallToolResult {
1085 |             is_error: Some(true),
1086 |             content: vec![Content {
1087 |                 annotations: None,
1088 |                 text: Some(
1089 |                     "Please provide project_id, merge_request_iid, title, and description".into(),
1090 |                 ),
1091 |                 mime_type: None,
1092 |                 r#type: ContentType::Text,
1093 |                 data: None,
1094 |             }],
1095 |         })
1096 |     }
1097 | }
1098 | 
1099 | fn gl_get_merge_request(input: CallToolRequest) -> Result<CallToolResult, Error> {
1100 |     let args = input.params.arguments.clone().unwrap_or_default();
1101 |     let (token, gitlab_url) = get_gitlab_config()?;
1102 | 
1103 |     if let (Some(Value::String(project_id)), Some(Value::String(merge_request_iid))) =
1104 |         (args.get("project_id"), args.get("merge_request_iid"))
1105 |     {
1106 |         let url = format!(
1107 |             "{}/projects/{}/merge_requests/{}",
1108 |             gitlab_url,
1109 |             urlencode_if_needed(project_id),
1110 |             merge_request_iid
1111 |         );
1112 | 
1113 |         let mut headers = BTreeMap::new();
1114 |         headers.insert("PRIVATE-TOKEN".to_string(), token);
1115 |         headers.insert("User-Agent".to_string(), "hyper-mcp/0.1.0".to_string());
1116 | 
1117 |         let req = HttpRequest {
1118 |             url,
1119 |             headers,
1120 |             method: Some("GET".to_string()),
1121 |         };
1122 | 
1123 |         let res = http::request::<()>(&req, None)?;
1124 | 
1125 |         if is_success_status(res.status_code()) {
1126 |             Ok(CallToolResult {
1127 |                 is_error: None,
1128 |                 content: vec![Content {
1129 |                     annotations: None,
1130 |                     text: Some(String::from_utf8_lossy(&res.body()).to_string()),
1131 |                     mime_type: Some("application/json".to_string()),
1132 |                     r#type: ContentType::Text,
1133 |                     data: None,
1134 |                 }],
1135 |             })
1136 |         } else {
1137 |             Ok(CallToolResult {
1138 |                 is_error: Some(true),
1139 |                 content: vec![Content {
1140 |                     annotations: None,
1141 |                     text: Some(format!(
1142 |                         "Failed to get merge request: {} - Response: {}",
1143 |                         res.status_code(),
1144 |                         String::from_utf8_lossy(&res.body())
1145 |                     )),
1146 |                     mime_type: None,
1147 |                     r#type: ContentType::Text,
1148 |                     data: None,
1149 |                 }],
1150 |             })
1151 |         }
1152 |     } else {
1153 |         Ok(CallToolResult {
1154 |             is_error: Some(true),
1155 |             content: vec![Content {
1156 |                 annotations: None,
1157 |                 text: Some("Please provide project_id and merge_request_iid".into()),
1158 |                 mime_type: None,
1159 |                 r#type: ContentType::Text,
1160 |                 data: None,
1161 |             }],
1162 |         })
1163 |     }
1164 | }
1165 | 
1166 | fn create_snippet(input: CallToolRequest) -> Result<CallToolResult, Error> {
1167 |     let args = input.params.arguments.clone().unwrap_or_default();
1168 |     let (token, gitlab_url) = get_gitlab_config()?;
1169 | 
1170 |     if let (Some(Value::String(title)), Some(Value::String(content))) =
1171 |         (args.get("title"), args.get("content"))
1172 |     {
1173 |         let url = format!("{}/snippets", gitlab_url);
1174 | 
1175 |         // Get visibility from args or default to "private"
1176 |         let visibility = args
1177 |             .get("visibility")
1178 |             .and_then(|v| v.as_str())
1179 |             .unwrap_or("private");
1180 | 
1181 |         let body = json!({
1182 |             "title": title,
1183 |             "file_name": format!("{}.txt", title.to_lowercase().replace(" ", "_")),
1184 |             "content": content,
1185 |             "visibility": visibility
1186 |         });
1187 | 
1188 |         let mut headers = BTreeMap::new();
1189 |         headers.insert("PRIVATE-TOKEN".to_string(), token);
1190 |         headers.insert("Content-Type".to_string(), "application/json".to_string());
1191 |         headers.insert("User-Agent".to_string(), "hyper-mcp/0.1.0".to_string());
1192 | 
1193 |         let req = HttpRequest {
1194 |             url,
1195 |             headers,
1196 |             method: Some("POST".to_string()),
1197 |         };
1198 | 
1199 |         let res = http::request(&req, Some(&body.to_string()))?;
1200 | 
1201 |         if is_success_status(res.status_code()) {
1202 |             Ok(CallToolResult {
1203 |                 is_error: None,
1204 |                 content: vec![Content {
1205 |                     annotations: None,
1206 |                     text: Some(String::from_utf8_lossy(&res.body()).to_string()),
1207 |                     mime_type: Some("application/json".to_string()),
1208 |                     r#type: ContentType::Text,
1209 |                     data: None,
1210 |                 }],
1211 |             })
1212 |         } else {
1213 |             Ok(CallToolResult {
1214 |                 is_error: Some(true),
1215 |                 content: vec![Content {
1216 |                     annotations: None,
1217 |                     text: Some(format!("Failed to create snippet: {}", res.status_code())),
1218 |                     mime_type: None,
1219 |                     r#type: ContentType::Text,
1220 |                     data: None,
1221 |                 }],
1222 |             })
1223 |         }
1224 |     } else {
1225 |         Ok(CallToolResult {
1226 |             is_error: Some(true),
1227 |             content: vec![Content {
1228 |                 annotations: None,
1229 |                 text: Some("Please provide title and content".into()),
1230 |                 mime_type: None,
1231 |                 r#type: ContentType::Text,
1232 |                 data: None,
1233 |             }],
1234 |         })
1235 |     }
1236 | }
1237 | 
1238 | fn update_snippet(input: CallToolRequest) -> Result<CallToolResult, Error> {
1239 |     let args = input.params.arguments.clone().unwrap_or_default();
1240 |     let (token, gitlab_url) = get_gitlab_config()?;
1241 | 
1242 |     if let (
1243 |         Some(Value::String(snippet_id)),
1244 |         Some(Value::String(title)),
1245 |         Some(Value::String(content)),
1246 |     ) = (
1247 |         args.get("snippet_id"),
1248 |         args.get("title"),
1249 |         args.get("content"),
1250 |     ) {
1251 |         let url = format!("{}/snippets/{}", gitlab_url, snippet_id);
1252 |         let body = json!({
1253 |             "title": title,
1254 |             "file_name": format!("{}.txt", title.to_lowercase().replace(" ", "_")),
1255 |             "content": content,
1256 |         });
1257 | 
1258 |         let mut headers = BTreeMap::new();
1259 |         headers.insert("PRIVATE-TOKEN".to_string(), token);
1260 |         headers.insert("Content-Type".to_string(), "application/json".to_string());
1261 |         headers.insert("User-Agent".to_string(), "hyper-mcp/0.1.0".to_string());
1262 | 
1263 |         let req = HttpRequest {
1264 |             url,
1265 |             headers,
1266 |             method: Some("PUT".to_string()),
1267 |         };
1268 | 
1269 |         let res = http::request(&req, Some(&body.to_string()))?;
1270 | 
1271 |         if is_success_status(res.status_code()) {
1272 |             Ok(CallToolResult {
1273 |                 is_error: None,
1274 |                 content: vec![Content {
1275 |                     annotations: None,
1276 |                     text: Some(String::from_utf8_lossy(&res.body()).to_string()),
1277 |                     mime_type: Some("application/json".to_string()),
1278 |                     r#type: ContentType::Text,
1279 |                     data: None,
1280 |                 }],
1281 |             })
1282 |         } else {
1283 |             Ok(CallToolResult {
1284 |                 is_error: Some(true),
1285 |                 content: vec![Content {
1286 |                     annotations: None,
1287 |                     text: Some(format!("Failed to update snippet: {}", res.status_code())),
1288 |                     mime_type: None,
1289 |                     r#type: ContentType::Text,
1290 |                     data: None,
1291 |                 }],
1292 |             })
1293 |         }
1294 |     } else {
1295 |         Ok(CallToolResult {
1296 |             is_error: Some(true),
1297 |             content: vec![Content {
1298 |                 annotations: None,
1299 |                 text: Some("Please provide snippet_id, title, and content".into()),
1300 |                 mime_type: None,
1301 |                 r#type: ContentType::Text,
1302 |                 data: None,
1303 |             }],
1304 |         })
1305 |     }
1306 | }
1307 | 
1308 | fn get_snippet(input: CallToolRequest) -> Result<CallToolResult, Error> {
1309 |     let args = input.params.arguments.clone().unwrap_or_default();
1310 |     let (token, gitlab_url) = get_gitlab_config()?;
1311 | 
1312 |     if let Some(Value::String(snippet_id)) = args.get("snippet_id") {
1313 |         let url = format!("{}/snippets/{}", gitlab_url, snippet_id);
1314 | 
1315 |         let mut headers = BTreeMap::new();
1316 |         headers.insert("PRIVATE-TOKEN".to_string(), token);
1317 |         headers.insert("User-Agent".to_string(), "hyper-mcp/0.1.0".to_string());
1318 | 
1319 |         let req = HttpRequest {
1320 |             url,
1321 |             headers,
1322 |             method: Some("GET".to_string()),
1323 |         };
1324 | 
1325 |         let res = http::request::<()>(&req, None)?;
1326 | 
1327 |         if is_success_status(res.status_code()) {
1328 |             Ok(CallToolResult {
1329 |                 is_error: None,
1330 |                 content: vec![Content {
1331 |                     annotations: None,
1332 |                     text: Some(String::from_utf8_lossy(&res.body()).to_string()),
1333 |                     mime_type: Some("application/json".to_string()),
1334 |                     r#type: ContentType::Text,
1335 |                     data: None,
1336 |                 }],
1337 |             })
1338 |         } else {
1339 |             Ok(CallToolResult {
1340 |                 is_error: Some(true),
1341 |                 content: vec![Content {
1342 |                     annotations: None,
1343 |                     text: Some(format!("Failed to get snippet: {}", res.status_code())),
1344 |                     mime_type: None,
1345 |                     r#type: ContentType::Text,
1346 |                     data: None,
1347 |                 }],
1348 |             })
1349 |         }
1350 |     } else {
1351 |         Ok(CallToolResult {
1352 |             is_error: Some(true),
1353 |             content: vec![Content {
1354 |                 annotations: None,
1355 |                 text: Some("Please provide snippet_id".into()),
1356 |                 mime_type: None,
1357 |                 r#type: ContentType::Text,
1358 |                 data: None,
1359 |             }],
1360 |         })
1361 |     }
1362 | }
1363 | 
1364 | fn delete_snippet(input: CallToolRequest) -> Result<CallToolResult, Error> {
1365 |     let args = input.params.arguments.clone().unwrap_or_default();
1366 |     let (token, gitlab_url) = get_gitlab_config()?;
1367 | 
1368 |     if let Some(Value::String(snippet_id)) = args.get("snippet_id") {
1369 |         let url = format!("{}/snippets/{}", gitlab_url, snippet_id);
1370 | 
1371 |         let mut headers = BTreeMap::new();
1372 |         headers.insert("PRIVATE-TOKEN".to_string(), token);
1373 |         headers.insert("User-Agent".to_string(), "hyper-mcp/0.1.0".to_string());
1374 | 
1375 |         let req = HttpRequest {
1376 |             url,
1377 |             headers,
1378 |             method: Some("DELETE".to_string()),
1379 |         };
1380 | 
1381 |         let res = http::request::<()>(&req, None)?;
1382 | 
1383 |         if is_success_status(res.status_code()) {
1384 |             Ok(CallToolResult {
1385 |                 is_error: None,
1386 |                 content: vec![Content {
1387 |                     annotations: None,
1388 |                     text: Some("Snippet deleted successfully".into()),
1389 |                     mime_type: None,
1390 |                     r#type: ContentType::Text,
1391 |                     data: None,
1392 |                 }],
1393 |             })
1394 |         } else {
1395 |             Ok(CallToolResult {
1396 |                 is_error: Some(true),
1397 |                 content: vec![Content {
1398 |                     annotations: None,
1399 |                     text: Some(format!("Failed to delete snippet: {}", res.status_code())),
1400 |                     mime_type: None,
1401 |                     r#type: ContentType::Text,
1402 |                     data: None,
1403 |                 }],
1404 |             })
1405 |         }
1406 |     } else {
1407 |         Ok(CallToolResult {
1408 |             is_error: Some(true),
1409 |             content: vec![Content {
1410 |                 annotations: None,
1411 |                 text: Some("Please provide snippet_id".into()),
1412 |                 mime_type: None,
1413 |                 r#type: ContentType::Text,
1414 |                 data: None,
1415 |             }],
1416 |         })
1417 |     }
1418 | }
1419 | 
1420 | fn gl_list_branches(input: CallToolRequest) -> Result<CallToolResult, Error> {
1421 |     let args = input.params.arguments.clone().unwrap_or_default();
1422 |     let (token, gitlab_url) = get_gitlab_config()?;
1423 | 
1424 |     if let Some(Value::String(project_id)) = args.get("project_id") {
1425 |         let url = format!(
1426 |             "{}/projects/{}/repository/branches",
1427 |             gitlab_url,
1428 |             urlencode_if_needed(project_id)
1429 |         );
1430 | 
1431 |         let mut headers = BTreeMap::new();
1432 |         headers.insert("PRIVATE-TOKEN".to_string(), token);
1433 |         headers.insert("User-Agent".to_string(), "hyper-mcp/0.1.0".to_string());
1434 | 
1435 |         let req = HttpRequest {
1436 |             url,
1437 |             headers,
1438 |             method: Some("GET".to_string()),
1439 |         };
1440 | 
1441 |         let res = http::request::<()>(&req, None)?;
1442 | 
1443 |         if is_success_status(res.status_code()) {
1444 |             Ok(CallToolResult {
1445 |                 is_error: None,
1446 |                 content: vec![Content {
1447 |                     annotations: None,
1448 |                     text: Some(String::from_utf8_lossy(&res.body()).to_string()),
1449 |                     mime_type: Some("application/json".to_string()),
1450 |                     r#type: ContentType::Text,
1451 |                     data: None,
1452 |                 }],
1453 |             })
1454 |         } else {
1455 |             Ok(CallToolResult {
1456 |                 is_error: Some(true),
1457 |                 content: vec![Content {
1458 |                     annotations: None,
1459 |                     text: Some(format!("Failed to list branches: {}", res.status_code())),
1460 |                     mime_type: None,
1461 |                     r#type: ContentType::Text,
1462 |                     data: None,
1463 |                 }],
1464 |             })
1465 |         }
1466 |     } else {
1467 |         Ok(CallToolResult {
1468 |             is_error: Some(true),
1469 |             content: vec![Content {
1470 |                 annotations: None,
1471 |                 text: Some("Please provide project_id".into()),
1472 |                 mime_type: None,
1473 |                 r#type: ContentType::Text,
1474 |                 data: None,
1475 |             }],
1476 |         })
1477 |     }
1478 | }
1479 | 
1480 | fn gl_list_issues(input: CallToolRequest) -> Result<CallToolResult, Error> {
1481 |     let args = input.params.arguments.clone().unwrap_or_default();
1482 |     let (token, gitlab_url) = get_gitlab_config()?;
1483 | 
1484 |     if let Some(Value::String(project_id)) = args.get("project_id") {
1485 |         let mut url_params = vec![];
1486 | 
1487 |         if let Some(Value::String(state)) = args.get("state") {
1488 |             url_params.push(format!("state={}", state));
1489 |         }
1490 |         if let Some(Value::String(labels)) = args.get("labels") {
1491 |             url_params.push(format!("labels={}", urlencoding::encode(labels)));
1492 |         }
1493 | 
1494 |         let query_string = if url_params.is_empty() {
1495 |             "".to_string()
1496 |         } else {
1497 |             format!("?{}", url_params.join("&"))
1498 |         };
1499 | 
1500 |         let url = format!(
1501 |             "{}/projects/{}/issues{}",
1502 |             gitlab_url,
1503 |             urlencode_if_needed(project_id),
1504 |             query_string
1505 |         );
1506 | 
1507 |         let mut headers = BTreeMap::new();
1508 |         headers.insert("PRIVATE-TOKEN".to_string(), token);
1509 |         headers.insert("User-Agent".to_string(), "hyper-mcp/0.1.0".to_string());
1510 | 
1511 |         let req = HttpRequest {
1512 |             url,
1513 |             headers,
1514 |             method: Some("GET".to_string()),
1515 |         };
1516 | 
1517 |         let res = http::request::<()>(&req, None)?;
1518 | 
1519 |         if is_success_status(res.status_code()) {
1520 |             Ok(CallToolResult {
1521 |                 is_error: None,
1522 |                 content: vec![Content {
1523 |                     annotations: None,
1524 |                     text: Some(String::from_utf8_lossy(&res.body()).to_string()),
1525 |                     mime_type: Some("application/json".to_string()),
1526 |                     r#type: ContentType::Text,
1527 |                     data: None,
1528 |                 }],
1529 |             })
1530 |         } else {
1531 |             Ok(CallToolResult {
1532 |                 is_error: Some(true),
1533 |                 content: vec![Content {
1534 |                     annotations: None,
1535 |                     text: Some(format!("Failed to list issues: {}", res.status_code())),
1536 |                     mime_type: None,
1537 |                     r#type: ContentType::Text,
1538 |                     data: None,
1539 |                 }],
1540 |             })
1541 |         }
1542 |     } else {
1543 |         Ok(CallToolResult {
1544 |             is_error: Some(true),
1545 |             content: vec![Content {
1546 |                 annotations: None,
1547 |                 text: Some("Please provide project_id".into()),
1548 |                 mime_type: None,
1549 |                 r#type: ContentType::Text,
1550 |                 data: None,
1551 |             }],
1552 |         })
1553 |     }
1554 | }
1555 | 
1556 | fn gl_get_repo_tree(input: CallToolRequest) -> Result<CallToolResult, Error> {
1557 |     let args = input.params.arguments.clone().unwrap_or_default();
1558 |     let (token, gitlab_url) = get_gitlab_config()?;
1559 | 
1560 |     if let Some(Value::String(project_id_val)) = args.get("project_id") {
1561 |         let project_id = project_id_val.as_str();
1562 |         let requested_path_opt = args.get("path").and_then(|v| v.as_str());
1563 |         let ref_name_opt = args.get("ref").and_then(|v| v.as_str());
1564 |         let recursive_opt = args.get("recursive").and_then(|v| v.as_bool());
1565 | 
1566 |         let mut all_entries: Vec<GitLabRepoEntry> = Vec::new();
1567 |         let mut current_page_number: u32 = 1;
1568 |         const PER_PAGE_COUNT: u32 = 100; // GitLab's typical max per_page
1569 |         const MAX_PAGES: u32 = 100; // Safety break: 100 pages * 100 items/page = 10,000 items
1570 | 
1571 |         loop {
1572 |             if current_page_number > MAX_PAGES {
1573 |                 // Log this or return a partial result with a warning if desired
1574 |                 // For now, just break and use what we have.
1575 |                 // Consider returning an error if this limit is hit.
1576 |                 break;
1577 |             }
1578 | 
1579 |             let mut url_params = vec![
1580 |                 format!("per_page={}", PER_PAGE_COUNT),
1581 |                 format!("page={}", current_page_number),
1582 |             ];
1583 | 
1584 |             if let Some(path_str) = requested_path_opt {
1585 |                 if !path_str.is_empty() {
1586 |                     url_params.push(format!("path={}", urlencoding::encode(path_str)));
1587 |                 }
1588 |             }
1589 |             if let Some(ref_name_str) = ref_name_opt {
1590 |                 url_params.push(format!("ref={}", urlencoding::encode(ref_name_str)));
1591 |             }
1592 |             if let Some(recursive_bool) = recursive_opt {
1593 |                 if recursive_bool {
1594 |                     url_params.push("recursive=true".to_string());
1595 |                 }
1596 |             }
1597 | 
1598 |             let query_string = format!("?{}", url_params.join("&"));
1599 |             let url = format!(
1600 |                 "{}/projects/{}/repository/tree{}",
1601 |                 gitlab_url,
1602 |                 urlencode_if_needed(project_id),
1603 |                 query_string
1604 |             );
1605 | 
1606 |             let mut headers = BTreeMap::new();
1607 |             headers.insert("PRIVATE-TOKEN".to_string(), token.clone()); // Clone token for loop
1608 |             headers.insert("User-Agent".to_string(), "hyper-mcp/0.1.0".to_string());
1609 | 
1610 |             let req = HttpRequest {
1611 |                 url: url.clone(),
1612 |                 headers,
1613 |                 method: Some("GET".to_string()),
1614 |             };
1615 | 
1616 |             let res = http::request::<()>(&req, None)?;
1617 | 
1618 |             if !is_success_status(res.status_code()) {
1619 |                 return Ok(CallToolResult {
1620 |                     is_error: Some(true),
1621 |                     content: vec![Content {
1622 |                         annotations: None,
1623 |                         text: Some(format!(
1624 |                             "Failed to get repository tree page {} from {}: {} - Response: {}",
1625 |                             current_page_number,
1626 |                             req.url,
1627 |                             res.status_code(),
1628 |                             String::from_utf8_lossy(&res.body())
1629 |                         )),
1630 |                         mime_type: None,
1631 |                         r#type: ContentType::Text,
1632 |                         data: None,
1633 |                     }],
1634 |                 });
1635 |             }
1636 | 
1637 |             match serde_json::from_slice::<Vec<GitLabRepoEntry>>(&res.body()) {
1638 |                 Ok(page_entries) => {
1639 |                     let num_fetched = page_entries.len();
1640 |                     all_entries.extend(page_entries);
1641 | 
1642 |                     if num_fetched < PER_PAGE_COUNT as usize {
1643 |                         break; // Last page fetched
1644 |                     }
1645 |                 }
1646 |                 Err(e) => {
1647 |                     return Ok(CallToolResult {
1648 |                         is_error: Some(true),
1649 |                         content: vec![Content {
1650 |                             annotations: None,
1651 |                             text: Some(format!(
1652 |                                 "Failed to parse repository tree data from GitLab API (page {}): {}",
1653 |                                 current_page_number, e
1654 |                             )),
1655 |                             mime_type: None,
1656 |                             r#type: ContentType::Text,
1657 |                             data: None,
1658 |                         }],
1659 |                     });
1660 |                 }
1661 |             }
1662 |             current_page_number += 1;
1663 |         }
1664 | 
1665 |         // Proceed with building the tree from all_entries
1666 |         match build_and_format_tree_from_entries(all_entries, requested_path_opt, project_id) {
1667 |             Ok(tree_string) => Ok(CallToolResult {
1668 |                 is_error: None,
1669 |                 content: vec![Content {
1670 |                     annotations: None,
1671 |                     text: Some(tree_string),
1672 |                     mime_type: Some("text/plain".to_string()),
1673 |                     r#type: ContentType::Text,
1674 |                     data: None,
1675 |                 }],
1676 |             }),
1677 |             Err(e_str) => Ok(CallToolResult {
1678 |                 is_error: Some(true),
1679 |                 content: vec![Content {
1680 |                     annotations: None,
1681 |                     text: Some(e_str),
1682 |                     mime_type: None,
1683 |                     r#type: ContentType::Text,
1684 |                     data: None,
1685 |                 }],
1686 |             }),
1687 |         }
1688 |     } else {
1689 |         Ok(CallToolResult {
1690 |             is_error: Some(true),
1691 |             content: vec![Content {
1692 |                 annotations: None,
1693 |                 text: Some("Please provide project_id".into()),
1694 |                 mime_type: None,
1695 |                 r#type: ContentType::Text,
1696 |                 data: None,
1697 |             }],
1698 |         })
1699 |     }
1700 | }
1701 | 
1702 | fn gl_get_repo_members(input: CallToolRequest) -> Result<CallToolResult, Error> {
1703 |     let args = input.params.arguments.clone().unwrap_or_default();
1704 |     let (token, gitlab_url) = get_gitlab_config()?;
1705 | 
1706 |     if let Some(Value::String(project_id_val)) = args.get("project_id") {
1707 |         let project_id = project_id_val.as_str();
1708 |         let include_inherited = args
1709 |             .get("include_inherited_members")
1710 |             .and_then(|v| v.as_bool())
1711 |             .unwrap_or(false);
1712 |         let query_opt = args.get("query").and_then(|v| v.as_str());
1713 | 
1714 |         let members_path = if include_inherited {
1715 |             "members/all"
1716 |         } else {
1717 |             "members"
1718 |         };
1719 | 
1720 |         let mut all_members_json: Vec<Value> = Vec::new();
1721 |         let mut current_page_number: u32 = 1;
1722 |         const PER_PAGE_COUNT: u32 = 100; // GitLab's typical max per_page
1723 |         const MAX_PAGES: u32 = 100; // Safety break: 100 pages * 100 items/page = 10,000 members
1724 | 
1725 |         loop {
1726 |             if current_page_number > MAX_PAGES {
1727 |                 // Log this or return a partial result with a warning if desired
1728 |                 break;
1729 |             }
1730 | 
1731 |             let mut url_params = vec![
1732 |                 format!("per_page={}", PER_PAGE_COUNT),
1733 |                 format!("page={}", current_page_number),
1734 |             ];
1735 | 
1736 |             if let Some(query_str) = query_opt {
1737 |                 url_params.push(format!("query={}", urlencoding::encode(query_str)));
1738 |             }
1739 | 
1740 |             let query_string = format!("?{}", url_params.join("&"));
1741 |             let url = format!(
1742 |                 "{}/projects/{}/{}{}",
1743 |                 gitlab_url,
1744 |                 urlencode_if_needed(project_id),
1745 |                 members_path,
1746 |                 query_string
1747 |             );
1748 | 
1749 |             let mut headers = BTreeMap::new();
1750 |             headers.insert("PRIVATE-TOKEN".to_string(), token.clone());
1751 |             headers.insert("User-Agent".to_string(), "hyper-mcp/0.1.0".to_string());
1752 | 
1753 |             let req = HttpRequest {
1754 |                 url: url.clone(),
1755 |                 headers,
1756 |                 method: Some("GET".to_string()),
1757 |             };
1758 | 
1759 |             let res = http::request::<()>(&req, None)?;
1760 | 
1761 |             if !is_success_status(res.status_code()) {
1762 |                 return Ok(CallToolResult {
1763 |                     is_error: Some(true),
1764 |                     content: vec![Content {
1765 |                         annotations: None,
1766 |                         text: Some(format!(
1767 |                             "Failed to get repository members page {} from {}: {} - Response: {}",
1768 |                             current_page_number,
1769 |                             req.url,
1770 |                             res.status_code(),
1771 |                             String::from_utf8_lossy(&res.body())
1772 |                         )),
1773 |                         mime_type: None,
1774 |                         r#type: ContentType::Text,
1775 |                         data: None,
1776 |                     }],
1777 |                 });
1778 |             }
1779 | 
1780 |             match serde_json::from_slice::<Vec<Value>>(&res.body()) {
1781 |                 Ok(page_members) => {
1782 |                     let num_fetched = page_members.len();
1783 |                     all_members_json.extend(page_members);
1784 | 
1785 |                     if num_fetched < PER_PAGE_COUNT as usize {
1786 |                         break; // Last page fetched
1787 |                     }
1788 |                 }
1789 |                 Err(e) => {
1790 |                     return Ok(CallToolResult {
1791 |                         is_error: Some(true),
1792 |                         content: vec![Content {
1793 |                             annotations: None,
1794 |                             text: Some(format!(
1795 |                                 "Failed to parse repository members data from GitLab API (page {}): {}",
1796 |                                 current_page_number, e
1797 |                             )),
1798 |                             mime_type: None,
1799 |                             r#type: ContentType::Text,
1800 |                             data: None,
1801 |                         }],
1802 |                     });
1803 |                 }
1804 |             }
1805 |             current_page_number += 1;
1806 |         }
1807 | 
1808 |         Ok(CallToolResult {
1809 |             is_error: None,
1810 |             content: vec![Content {
1811 |                 annotations: None,
1812 |                 text: Some(serde_json::to_string(&all_members_json)?),
1813 |                 mime_type: Some("application/json".to_string()),
1814 |                 r#type: ContentType::Text,
1815 |                 data: None,
1816 |             }],
1817 |         })
1818 |     } else {
1819 |         Ok(CallToolResult {
1820 |             is_error: Some(true),
1821 |             content: vec![Content {
1822 |                 annotations: None,
1823 |                 text: Some("Please provide project_id".into()),
1824 |                 mime_type: None,
1825 |                 r#type: ContentType::Text,
1826 |                 data: None,
1827 |             }],
1828 |         })
1829 |     }
1830 | }
1831 | 
1832 | pub(crate) fn describe() -> Result<ListToolsResult, Error> {
1833 |     Ok(ListToolsResult {
1834 |         tools: vec![
1835 |             ToolDescription {
1836 |                 name: "gl_delete_file".into(),
1837 |                 description: "Delete a file in a GitLab project repository. Requires project_id, file_path, branch, and optional commit_message.".into(),
1838 |                 input_schema: json!({
1839 |                     "type": "object",
1840 |                     "properties": {
1841 |                         "project_id": {
1842 |                             "type": "string",
1843 |                             "description": "The project identifier - can be a numeric project ID (e.g. '123') or a URL-encoded path (e.g. 'group%2Fproject')",
1844 |                         },
1845 |                         "file_path": {
1846 |                             "type": "string",
1847 |                             "description": "The path to the file in the project",
1848 |                         },
1849 |                         "branch": {
1850 |                             "type": "string",
1851 |                             "description": "The name of the branch to delete the file from",
1852 |                         },
1853 |                         "commit_message": {
1854 |                             "type": "string",
1855 |                             "description": "The commit message. Optional, defaults to 'Delete file via API'",
1856 |                         },
1857 |                         "author_email": {
1858 |                             "type": "string",
1859 |                             "description": "The email of the commit author. Optional.",
1860 |                         },
1861 |                         "author_name": {
1862 |                             "type": "string",
1863 |                             "description": "The name of the commit author. Optional.",
1864 |                         },
1865 |                     },
1866 |                     "required": ["project_id", "file_path", "branch"],
1867 |                 })
1868 |                 .as_object()
1869 |                 .unwrap()
1870 |                 .clone(),
1871 |             },
1872 |             ToolDescription {
1873 |                 name: "gl_create_issue".into(),
1874 |                 description: "Create a new issue in a GitLab project".into(),
1875 |                 input_schema: json!({
1876 |                     "type": "object",
1877 |                     "properties": {
1878 |                         "project_id": {
1879 |                             "type": "string",
1880 |                             "description": "The project identifier - can be a numeric project ID (e.g. '123') or a URL-encoded path (e.g. 'group%2Fproject')",
1881 |                         },
1882 |                         "title": {
1883 |                             "type": "string",
1884 |                             "description": "The title of the issue",
1885 |                         },
1886 |                         "description": {
1887 |                             "type": "string",
1888 |                             "description": "The description of the issue",
1889 |                         },
1890 |                         "labels": {
1891 |                             "type": "string",
1892 |                             "description": "Comma-separated list of labels",
1893 |                         },
1894 |                     },
1895 |                     "required": ["project_id", "title", "description"],
1896 |                 })
1897 |                 .as_object()
1898 |                 .unwrap()
1899 |                 .clone(),
1900 |             },
1901 |             ToolDescription {
1902 |                 name: "gl_get_issue".into(),
1903 |                 description: "Get details of a specific issue".into(),
1904 |                 input_schema: json!({
1905 |                     "type": "object",
1906 |                     "properties": {
1907 |                         "project_id": {
1908 |                             "type": "string",
1909 |                             "description": "The project identifier - can be a numeric project ID (e.g. '123') or a URL-encoded path (e.g. 'group%2Fproject')",
1910 |                         },
1911 |                         "issue_iid": {
1912 |                             "type": "string",
1913 |                             "description": "The internal ID of the issue",
1914 |                         },
1915 |                     },
1916 |                     "required": ["project_id", "issue_iid"],
1917 |                 })
1918 |                 .as_object()
1919 |                 .unwrap()
1920 |                 .clone(),
1921 |             },
1922 |             ToolDescription {
1923 |                 name: "gl_update_issue".into(),
1924 |                 description: "Update an existing issue in a GitLab project".into(),
1925 |                 input_schema: json!({
1926 |                     "type": "object",
1927 |                     "properties": {
1928 |                         "project_id": {
1929 |                             "type": "string",
1930 |                             "description": "The project identifier - can be a numeric project ID (e.g. '123') or a URL-encoded path (e.g. 'group%2Fproject')",
1931 |                         },
1932 |                         "issue_iid": {
1933 |                             "type": "string",
1934 |                             "description": "The internal ID of the issue",
1935 |                         },
1936 |                         "title": {
1937 |                             "type": "string",
1938 |                             "description": "The new title of the issue",
1939 |                         },
1940 |                         "description": {
1941 |                             "type": "string",
1942 |                             "description": "The new description of the issue",
1943 |                         },
1944 |                         "add_labels": {
1945 |                             "type": "string",
1946 |                             "description": "Comma-separated list of labels to add to the issue",
1947 |                         },
1948 |                         "remove_labels": {
1949 |                             "type": "string",
1950 |                             "description": "Comma-separated list of labels to remove from the issue",
1951 |                         },
1952 |                         "due_date": {
1953 |                             "type": "string",
1954 |                             "description": "The due date of the issue in YYYY-MM-DD format (e.g., 2024-03-11)",
1955 |                         },
1956 |                     },
1957 |                     "required": ["project_id", "issue_iid"],
1958 |                 })
1959 |                 .as_object()
1960 |                 .unwrap()
1961 |                 .clone(),
1962 |             },
1963 |             ToolDescription {
1964 |                 name: "gl_add_issue_comment".into(),
1965 |                 description: "Add a comment to an issue in a GitLab project".into(),
1966 |                 input_schema: json!({
1967 |                     "type": "object",
1968 |                     "properties": {
1969 |                         "project_id": {
1970 |                             "type": "string",
1971 |                             "description": "The project identifier - can be a numeric project ID (e.g. '123') or a URL-encoded path (e.g. 'group%2Fproject')",
1972 |                         },
1973 |                         "issue_iid": {
1974 |                             "type": "string",
1975 |                             "description": "The internal ID of the issue",
1976 |                         },
1977 |                         "comment": {
1978 |                             "type": "string",
1979 |                             "description": "The comment to add to the issue",
1980 |                         },
1981 |                     },
1982 |                     "required": ["project_id", "issue_iid", "comment"],
1983 |                 })
1984 |                 .as_object()
1985 |                 .unwrap()
1986 |                 .clone(),
1987 |             },
1988 |             ToolDescription {
1989 |                 name: "gl_list_issues".into(),
1990 |                 description: "List issues for a project in GitLab. Supports filtering by state and labels.".into(),
1991 |                 input_schema: json!({
1992 |                     "type": "object",
1993 |                     "properties": {
1994 |                         "project_id": {
1995 |                             "type": "string",
1996 |                             "description": "The project identifier - can be a numeric project ID (e.g. '123') or a URL-encoded path (e.g. 'group%2Fproject')",
1997 |                         },
1998 |                         "state": {
1999 |                             "type": "string",
2000 |                             "description": "Filter by state: 'opened', 'closed', or 'all'. Defaults to 'opened' if not specified by GitLab.",
2001 |                         },
2002 |                         "labels": {
2003 |                             "type": "string",
2004 |                             "description": "Comma-separated list of label names to filter by.",
2005 |                         },
2006 |                     },
2007 |                     "required": ["project_id"],
2008 |                 })
2009 |                 .as_object()
2010 |                 .unwrap()
2011 |                 .clone(),
2012 |             },
2013 |             ToolDescription {
2014 |                 name: "gl_get_file_contents".into(),
2015 |                 description: "Get the contents of a file in a GitLab project".into(),
2016 |                 input_schema: json!({
2017 |                     "type": "object",
2018 |                     "properties": {
2019 |                         "project_id": {
2020 |                             "type": "string",
2021 |                             "description": "The project identifier - can be a numeric project ID (e.g. '123') or a URL-encoded path (e.g. 'group%2Fproject')",
2022 |                         },
2023 |                         "file_path": {
2024 |                             "type": "string",
2025 |                             "description": "The path to the file in the project",
2026 |                         },
2027 |                         "ref": {
2028 |                             "type": "string",
2029 |                             "description": "The name of the branch, tag or commit (defaults to HEAD)",
2030 |                         },
2031 |                     },
2032 |                     "required": ["project_id", "file_path"],
2033 |                 })
2034 |                 .as_object()
2035 |                 .unwrap()
2036 |                 .clone(),
2037 |             },
2038 |             ToolDescription {
2039 |                 name: "gl_create_or_update_file".into(),
2040 |                 description: "Create or update a file in a GitLab project".into(),
2041 |                 input_schema: json!({
2042 |                     "type": "object",
2043 |                     "properties": {
2044 |                         "project_id": {
2045 |                             "type": "string",
2046 |                             "description": "The project identifier - can be a numeric project ID (e.g. '123') or a URL-encoded path (e.g. 'group%2Fproject')",
2047 |                         },
2048 |                         "file_path": {
2049 |                             "type": "string",
2050 |                             "description": "The path to the file in the project",
2051 |                         },
2052 |                         "content": {
2053 |                             "type": "string",
2054 |                             "description": "The content to write to the file",
2055 |                         },
2056 |                         "branch": {
2057 |                             "type": "string",
2058 |                             "description": "The name of the branch to create or update the file in",
2059 |                         },
2060 |                         "author_email": {
2061 |                             "type": "string",
2062 |                             "description": "The email of the commit author",
2063 |                         },
2064 |                         "author_name": {
2065 |                             "type": "string",
2066 |                             "description": "The name of the commit author",
2067 |                         },
2068 |                         "commit_message": {
2069 |                             "type": "string",
2070 |                             "description": "The commit message. Defaults to 'Update file via API' if not specified.",
2071 |                         },
2072 |                     },
2073 |                     "required": ["project_id", "file_path", "content", "branch"],
2074 |                 })
2075 |                 .as_object()
2076 |                 .unwrap()
2077 |                 .clone(),
2078 |             },
2079 |             ToolDescription {
2080 |                 name: "gl_create_branch".into(),
2081 |                 description: "Create a new branch in a GitLab project".into(),
2082 |                 input_schema: json!({
2083 |                     "type": "object",
2084 |                     "properties": {
2085 |                         "project_id": {
2086 |                             "type": "string",
2087 |                             "description": "The project identifier - can be a numeric project ID (e.g. '123') or a URL-encoded path (e.g. 'group%2Fproject')",
2088 |                         },
2089 |                         "branch_name": {
2090 |                             "type": "string",
2091 |                             "description": "The name of the new branch",
2092 |                         },
2093 |                         "ref": {
2094 |                             "type": "string",
2095 |                             "description": "The branch name or commit SHA to create the new branch from",
2096 |                         },
2097 |                     },
2098 |                     "required": ["project_id", "branch_name", "ref"],
2099 |                 })
2100 |                 .as_object()
2101 |                 .unwrap()
2102 |                 .clone(),
2103 |             },
2104 |             ToolDescription {
2105 |                 name: "gl_create_merge_request".into(),
2106 |                 description: "Create a new merge request in a GitLab project".into(),
2107 |                 input_schema: json!({
2108 |                     "type": "object",
2109 |                     "properties": {
2110 |                         "project_id": {
2111 |                             "type": "string",
2112 |                             "description": "The project identifier - can be a numeric project ID (e.g. '123') or a URL-encoded path (e.g. 'group%2Fproject')",
2113 |                         },
2114 |                         "source_branch": {
2115 |                             "type": "string",
2116 |                             "description": "The name of the source branch",
2117 |                         },
2118 |                         "target_branch": {
2119 |                             "type": "string",
2120 |                             "description": "The name of the target branch",
2121 |                         },
2122 |                     },
2123 |                     "required": ["project_id", "source_branch", "target_branch"],
2124 |                 })
2125 |                 .as_object()
2126 |                 .unwrap()
2127 |                 .clone(),
2128 |             },
2129 |             ToolDescription {
2130 |                 name: "gl_update_merge_request".into(),
2131 |                 description: "Update an existing merge request in a GitLab project.".into(),
2132 |                 input_schema: json!({
2133 |                     "type": "object",
2134 |                     "properties": {
2135 |                         "project_id": {
2136 |                             "type": "string",
2137 |                             "description": "The project identifier - can be a numeric project ID (e.g. '123') or a URL-encoded path (e.g. 'group%2Fproject')",
2138 |                         },
2139 |                         "merge_request_iid": {
2140 |                             "type": "string",
2141 |                             "description": "The internal ID (IID) of the merge request to update",
2142 |                         },
2143 |                         "title": {
2144 |                             "type": "string",
2145 |                             "description": "The new title for the merge request.",
2146 |                         },
2147 |                         "description": {
2148 |                             "type": "string",
2149 |                             "description": "The new description for the merge request.",
2150 |                         },
2151 |                         // Consider adding other common updatable fields like:
2152 |                         // "target_branch": { "type": "string", "description": "The target branch" },
2153 |                         // "state_event": { "type": "string", "description": "Event to change MR state (e.g., 'close', 'reopen')" }
2154 |                     },
2155 |                     "required": ["project_id", "merge_request_iid", "title", "description"],
2156 |                 })
2157 |                 .as_object()
2158 |                 .unwrap()
2159 |                 .clone(),
2160 |             },
2161 |             ToolDescription {
2162 |                 name: "gl_get_merge_request".into(),
2163 |                 description: "Get details of a specific merge request in a GitLab project.".into(),
2164 |                 input_schema: json!({
2165 |                     "type": "object",
2166 |                     "properties": {
2167 |                         "project_id": {
2168 |                             "type": "string",
2169 |                             "description": "The project identifier - can be a numeric project ID (e.g. '123') or a URL-encoded path (e.g. 'group%2Fproject')",
2170 |                         },
2171 |                         "merge_request_iid": {
2172 |                             "type": "string",
2173 |                             "description": "The internal ID (IID) of the merge request",
2174 |                         },
2175 |                     },
2176 |                     "required": ["project_id", "merge_request_iid"],
2177 |                 })
2178 |                 .as_object()
2179 |                 .unwrap()
2180 |                 .clone(),
2181 |             },
2182 |             ToolDescription {
2183 |                 name: "gl_create_snippet".into(),
2184 |                 description: "Create a new snippet".into(),
2185 |                 input_schema: json!({
2186 |                     "type": "object",
2187 |                     "properties": {
2188 |                         "title": {
2189 |                             "type": "string",
2190 |                             "description": "The title of the snippet",
2191 |                         },
2192 |                         "content": {
2193 |                             "type": "string",
2194 |                             "description": "The content of the snippet",
2195 |                         },
2196 |                         "visibility": {
2197 |                             "type": "string",
2198 |                             "description": "The visibility level of the snippet (private, internal, or public). Defaults to private if not specified.",
2199 |                         },
2200 |                     },
2201 |                     "required": ["title", "content"],
2202 |                 })
2203 |                 .as_object()
2204 |                 .unwrap()
2205 |                 .clone(),
2206 |             },
2207 |             ToolDescription {
2208 |                 name: "gl_update_snippet".into(),
2209 |                 description: "Update an existing snippet".into(),
2210 |                 input_schema: json!({
2211 |                     "type": "object",
2212 |                     "properties": {
2213 |                         "snippet_id": {
2214 |                             "type": "string",
2215 |                             "description": "The ID of the snippet",
2216 |                         },
2217 |                         "title": {
2218 |                             "type": "string",
2219 |                             "description": "The new title of the snippet",
2220 |                         },
2221 |                         "content": {
2222 |                             "type": "string",
2223 |                             "description": "The new content of the snippet",
2224 |                         },
2225 |                     },
2226 |                     "required": ["snippet_id", "title", "content"],
2227 |                 })
2228 |                 .as_object()
2229 |                 .unwrap()
2230 |                 .clone(),
2231 |             },
2232 |             ToolDescription {
2233 |                 name: "gl_get_snippet".into(),
2234 |                 description: "Get details of a specific snippet".into(),
2235 |                 input_schema: json!({
2236 |                     "type": "object",
2237 |                     "properties": {
2238 |                         "snippet_id": {
2239 |                             "type": "string",
2240 |                             "description": "The ID of the snippet",
2241 |                         },
2242 |                     },
2243 |                     "required": ["snippet_id"],
2244 |                 })
2245 |                 .as_object()
2246 |                 .unwrap()
2247 |                 .clone(),
2248 |             },
2249 |             ToolDescription {
2250 |                 name: "gl_delete_snippet".into(),
2251 |                 description: "Delete a snippet".into(),
2252 |                 input_schema: json!({
2253 |                     "type": "object",
2254 |                     "properties": {
2255 |                         "snippet_id": {
2256 |                             "type": "string",
2257 |                             "description": "The ID of the snippet",
2258 |                         },
2259 |                     },
2260 |                     "required": ["snippet_id"],
2261 |                 })
2262 |                 .as_object()
2263 |                 .unwrap()
2264 |                 .clone(),
2265 |             },
2266 |             ToolDescription {
2267 |                 name: "gl_list_branches".into(),
2268 |                 description: "List all branches in a GitLab project".into(),
2269 |                 input_schema: json!({
2270 |                     "type": "object",
2271 |                     "properties": {
2272 |                         "project_id": {
2273 |                             "type": "string",
2274 |                             "description": "The project identifier - can be a numeric project ID (e.g. '123') or a URL-encoded path (e.g. 'group%2Fproject')",
2275 |                         },
2276 |                     },
2277 |                     "required": ["project_id"],
2278 |                 })
2279 |                 .as_object()
2280 |                 .unwrap()
2281 |                 .clone(),
2282 |             },
2283 |             ToolDescription {
2284 |                 name: "gl_get_repo_tree".into(),
2285 |                 description: "Get the list of files and directories in a project repository. Handles pagination internally.".into(),
2286 |                 input_schema: json!({
2287 |                     "type": "object",
2288 |                     "properties": {
2289 |                         "project_id": {
2290 |                             "type": "string",
2291 |                             "description": "The project identifier - can be a numeric project ID (e.g. '123') or a URL-encoded path (e.g. 'group%2Fproject')",
2292 |                         },
2293 |                         "path": {
2294 |                             "type": "string",
2295 |                             "description": "The path inside the repository. Used to get content of subdirectories. Optional.",
2296 |                         },
2297 |                         "ref": {
2298 |                             "type": "string",
2299 |                             "description": "The name of a repository branch or tag or if not given the default branch. Optional.",
2300 |                         },
2301 |                         "recursive": {
2302 |                             "type": "boolean",
2303 |                             "description": "Boolean value used to get a recursive tree. If you want a complete tree, set this to true. Default is false. Optional.",
2304 |                         },
2305 |                     },
2306 |                     "required": ["project_id"],
2307 |                 })
2308 |                 .as_object()
2309 |                 .unwrap()
2310 |                 .clone(),
2311 |             },
2312 |             ToolDescription {
2313 |                 name: "gl_get_repo_members".into(),
2314 |                 description: "Get a list of members for a GitLab project. Supports fetching direct or inherited members and filtering by query. Handles pagination internally.".into(),
2315 |                 input_schema: json!({
2316 |                     "type": "object",
2317 |                     "properties": {
2318 |                         "project_id": {
2319 |                             "type": "string",
2320 |                             "description": "The project identifier - can be a numeric project ID (e.g. '123') or a URL-encoded path (e.g. 'group%2Fproject')",
2321 |                         },
2322 |                         "include_inherited_members": {
2323 |                             "type": "boolean",
2324 |                             "description": "Set to true to include inherited members (e.g., from groups). Defaults to false (direct members only). Optional.",
2325 |                         },
2326 |                         "query": {
2327 |                             "type": "string",
2328 |                             "description": "Filter by username, name, or public email. Optional.",
2329 |                         },
2330 |                     },
2331 |                     "required": ["project_id"],
2332 |                 })
2333 |                 .as_object()
2334 |                 .unwrap()
2335 |                 .clone(),
2336 |             },
2337 |         ],
2338 |     })
2339 | }
2340 | 
```
Page 8/11FirstPrevNextLast