#
tokens: 24985/50000 9/9 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── attachment.go
├── go.mod
├── go.sum
├── LICENSE
├── main.go
├── README.md
├── sprint.go
├── tag.go
├── template.go
├── wiki.go
└── workitems.go
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | mcp-azuredevops-bridge
2 | mcp-azuredevops-bridge.exe
3 | start.sh
4 | test-wiki-api.ps1
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # MCP Azure DevOps Bridge
  2 | 
  3 | A Model Context Protocol (MCP) integration server for Azure DevOps. This focused integration allows you to manage work items, wiki documentation, sprint planning, and handle attachments and discussions seamlessly.
  4 | 
  5 | ## 🌉 Azure DevOps Integration
  6 | 
  7 | Connect with Azure DevOps for comprehensive project management:
  8 | 
  9 | - **Work Items** - Create, update, query, and manage work items
 10 | - **Wiki Documentation** - Create, update, and retrieve wiki pages
 11 | - **Sprint Planning** - Retrieve current sprint information and list sprints
 12 | - **Attachments & Discussions** - Add and retrieve attachments and comments to/from work items
 13 | 
 14 | ## 🚀 Getting Started
 15 | 
 16 | ### Prerequisites
 17 | 
 18 | - Go 1.23 or later
 19 | - Azure DevOps Personal Access Token (PAT)
 20 | 
 21 | ### Installation
 22 | 
 23 | #### Installing Go 1.23 or above
 24 | 
 25 | ##### Windows
 26 | Install Go using one of these package managers:
 27 | 
 28 | 1. Using **winget**:
 29 |    ```
 30 |    winget install GoLang.Go
 31 |    ```
 32 | 
 33 | 2. Using **Chocolatey**:
 34 |    ```
 35 |    choco install golang
 36 |    ```
 37 | 
 38 | 3. Using **Scoop**:
 39 |    ```
 40 |    scoop install go
 41 |    ```
 42 | 
 43 | After installation, verify with:
 44 | ```
 45 | go version
 46 | ```
 47 | 
 48 | ##### macOS
 49 | Install Go using Homebrew:
 50 | 
 51 | ```
 52 | brew install go
 53 | ```
 54 | 
 55 | Verify the installation:
 56 | ```
 57 | go version
 58 | ```
 59 | 
 60 | #### Building the Project
 61 | 
 62 | 1. Clone and build:
 63 | 
 64 | ```bash
 65 | git clone https://github.com/krishh-amilineni/mcp-azuredevops-bridge.git
 66 | cd mcp-azuredevops-bridge
 67 | go build
 68 | ```
 69 | 
 70 | 2. Configure your environment:
 71 | 
 72 | ```bash
 73 | export AZURE_DEVOPS_ORG="your-org"
 74 | export AZDO_PAT="your-pat-token"
 75 | export AZURE_DEVOPS_PROJECT="your-project"
 76 | ```
 77 | 
 78 | 3. Add to your Windsurf / Cursor configuration:
 79 | 
 80 | ```json
 81 | {
 82 |   "mcpServers": {
 83 |     "azuredevops-bridge": {
 84 |       "command": "/full/path/to/mcp-azuredevops-bridge/mcp-azuredevops-bridge",
 85 |       "args": [],
 86 |       "env": {
 87 |         "AZURE_DEVOPS_ORG": "organization",
 88 |         "AZDO_PAT": "personal_access_token",
 89 |         "AZURE_DEVOPS_PROJECT": "project"
 90 |       }
 91 |     }
 92 |   }
 93 | }
 94 | ```
 95 | 
 96 | ## 💡 Example Workflows
 97 | 
 98 | ### Work Item Management
 99 | 
100 | ```txt
101 | "Create a user story for the new authentication feature in Azure DevOps"
102 | ```
103 | 
104 | ### Wiki Documentation
105 | 
106 | ```txt
107 | "Create a wiki page documenting the API endpoints for our service"
108 | "List all wiki pages in our project wiki"
109 | "Get the content of the 'Getting Started' page from the wiki"
110 | "Show me all available wikis in my Azure DevOps project"
111 | ```
112 | 
113 | ### Sprint Planning
114 | 
115 | ```txt
116 | "Show me the current sprint's work items and their status"
117 | ```
118 | 
119 | ### Attachments and Comments
120 | 
121 | ```txt
122 | "Add this screenshot as an attachment to work item #123"
123 | ```
124 | 
125 | ## 🔧 Features
126 | 
127 | ### Work Item Management
128 | - Create new work items (user stories, bugs, tasks, etc.)
129 | - Update existing work items
130 | - Query work items by various criteria
131 | - Link work items to each other
132 | 
133 | ### Wiki Management
134 | - Create and update wiki pages
135 | - Search wiki content
136 | - Retrieve page content and subpages
137 | - Automatic wiki discovery - dynamically finds all available wikis for your project
138 | - Smart wiki selection - selects the most appropriate wiki based on the project context
139 | - Get list of available wikis for debugging and exploration
140 | 
141 | ### Sprint Management
142 | - Get current sprint information
143 | - List all sprints
144 | - View sprint statistics
145 | 
146 | ### Attachments and Comments
147 | - Add attachments to work items
148 | - Retrieve attachments from work items
149 | - Add comments to work items
150 | - View comments on work items
151 | 
152 | ## 📋 Advanced Wiki Usage
153 | 
154 | The DevOps Bridge includes enhanced wiki functionality that can help you access documentation more effectively:
155 | 
156 | ### Available Wiki Tools
157 | 
158 | - `list_wiki_pages` - Lists all wiki pages, optionally from a specific path
159 | - `get_wiki_page` - Retrieves the content of a specific wiki page
160 | - `manage_wiki_page` - Creates or updates a wiki page
161 | - `search_wiki` - Searches for content across wiki pages
162 | - `get_available_wikis` - Lists all available wikis in your Azure DevOps organization
163 | 
164 | ### Wiki Troubleshooting
165 | 
166 | If you're having trouble accessing wiki content:
167 | 
168 | 1. Use the `get_available_wikis` tool to see all available wikis and their IDs
169 | 2. Check that your PAT token has appropriate permissions for wiki access
170 | 3. Verify that the wiki path is correct - wiki paths are case-sensitive
171 | 4. Enable verbose logging to see detailed request and response information
172 | 
173 | ## 🔒 Security
174 | 
175 | This integration uses Personal Access Tokens (PAT) for authenticating with Azure DevOps. Ensure your PAT has the appropriate permissions for the operations you want to perform.
176 | 
177 | ## 📝 Credits
178 | 
179 | This project was inspired by and draws from the original work at [TheApeMachine/mcp-server-devops-bridge](https://github.com/TheApeMachine/mcp-server-devops-bridge). We appreciate their contribution to the open source community.
180 | 
181 | ## 📝 License
182 | 
183 | This project is licensed under the MIT License - see the LICENSE file for details.
184 | 
185 | ## 🤝 Contributing
186 | 
187 | Contributions are welcome! Please feel free to submit a Pull Request.
188 | 
189 | 1. Fork the repository
190 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
191 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
192 | 4. Push to the branch (`git push origin feature/amazing-feature`)
193 | 5. Open a Pull Request
194 | 
```

--------------------------------------------------------------------------------
/tag.go:
--------------------------------------------------------------------------------

```go
  1 | package main
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 	"strings"
  7 | 
  8 | 	"github.com/mark3labs/mcp-go/mcp"
  9 | 	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/webapi"
 10 | 	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/workitemtracking"
 11 | )
 12 | 
 13 | // Handler for managing work item tags
 14 | func handleManageWorkItemTags(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 15 | 	id := int(request.Params.Arguments["id"].(float64))
 16 | 	operation := request.Params.Arguments["operation"].(string)
 17 | 	tagsStr := request.Params.Arguments["tags"].(string)
 18 | 	tags := strings.Split(tagsStr, ",")
 19 | 
 20 | 	// Get current work item to get existing tags
 21 | 	workItem, err := workItemClient.GetWorkItem(ctx, workitemtracking.GetWorkItemArgs{
 22 | 		Id:      &id,
 23 | 		Project: &config.Project,
 24 | 	})
 25 | 	if err != nil {
 26 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to get work item: %v", err)), nil
 27 | 	}
 28 | 
 29 | 	fields := *workItem.Fields
 30 | 	var currentTags []string
 31 | 	if tags, ok := fields["System.Tags"].(string); ok && tags != "" {
 32 | 		currentTags = strings.Split(tags, "; ")
 33 | 	}
 34 | 
 35 | 	var newTags []string
 36 | 	switch operation {
 37 | 	case "add":
 38 | 		// Add new tags while avoiding duplicates
 39 | 		tagMap := make(map[string]bool)
 40 | 		for _, tag := range currentTags {
 41 | 			tagMap[strings.TrimSpace(tag)] = true
 42 | 		}
 43 | 		for _, tag := range tags {
 44 | 			tagMap[strings.TrimSpace(tag)] = true
 45 | 		}
 46 | 		for tag := range tagMap {
 47 | 			newTags = append(newTags, tag)
 48 | 		}
 49 | 	case "remove":
 50 | 		// Remove specified tags
 51 | 		tagMap := make(map[string]bool)
 52 | 		for _, tag := range tags {
 53 | 			tagMap[strings.TrimSpace(tag)] = true
 54 | 		}
 55 | 		for _, tag := range currentTags {
 56 | 			if !tagMap[strings.TrimSpace(tag)] {
 57 | 				newTags = append(newTags, tag)
 58 | 			}
 59 | 		}
 60 | 	}
 61 | 
 62 | 	// Update work item with new tags
 63 | 	updateArgs := workitemtracking.UpdateWorkItemArgs{
 64 | 		Id:      &id,
 65 | 		Project: &config.Project,
 66 | 		Document: &[]webapi.JsonPatchOperation{
 67 | 			{
 68 | 				Op:    &webapi.OperationValues.Replace,
 69 | 				Path:  stringPtr("/fields/System.Tags"),
 70 | 				Value: strings.Join(newTags, "; "),
 71 | 			},
 72 | 		},
 73 | 	}
 74 | 
 75 | 	_, err = workItemClient.UpdateWorkItem(ctx, updateArgs)
 76 | 	if err != nil {
 77 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to update tags: %v", err)), nil
 78 | 	}
 79 | 
 80 | 	return mcp.NewToolResultText(fmt.Sprintf("Successfully %sd tags for work item #%d", operation, id)), nil
 81 | }
 82 | 
 83 | // Handler for getting work item tags
 84 | func handleGetWorkItemTags(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 85 | 	id := int(request.Params.Arguments["id"].(float64))
 86 | 
 87 | 	workItem, err := workItemClient.GetWorkItem(ctx, workitemtracking.GetWorkItemArgs{
 88 | 		Id:      &id,
 89 | 		Project: &config.Project,
 90 | 	})
 91 | 	if err != nil {
 92 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to get work item: %v", err)), nil
 93 | 	}
 94 | 
 95 | 	fields := *workItem.Fields
 96 | 	if tags, ok := fields["System.Tags"].(string); ok && tags != "" {
 97 | 		return mcp.NewToolResultText(fmt.Sprintf("Tags for work item #%d:\n%s", id, tags)), nil
 98 | 	}
 99 | 
100 | 	return mcp.NewToolResultText(fmt.Sprintf("No tags found for work item #%d", id)), nil
101 | }
102 | 
```

--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------

```go
  1 | package main
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 	"log"
  7 | 	"os"
  8 | 	"strings"
  9 | 
 10 | 	"github.com/mark3labs/mcp-go/server"
 11 | 	"github.com/microsoft/azure-devops-go-api/azuredevops/v7"
 12 | 	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/core"
 13 | 	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/wiki"
 14 | 	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/workitemtracking"
 15 | )
 16 | 
 17 | // AzureDevOpsConfig holds the configuration for Azure DevOps connection
 18 | type AzureDevOpsConfig struct {
 19 | 	OrganizationURL     string
 20 | 	PersonalAccessToken string
 21 | 	Project             string
 22 | }
 23 | 
 24 | // Global clients and config
 25 | var (
 26 | 	connection     *azuredevops.Connection
 27 | 	workItemClient workitemtracking.Client
 28 | 	wikiClient     wiki.Client
 29 | 	coreClient     core.Client
 30 | 	config         AzureDevOpsConfig
 31 | )
 32 | 
 33 | func main() {
 34 | 	// Main function for the MCP server - handles initialization and startup
 35 | 	// Load configuration from environment variables
 36 | 	config = AzureDevOpsConfig{
 37 | 		OrganizationURL:     "https://dev.azure.com/" + os.Getenv("AZURE_DEVOPS_ORG"),
 38 | 		PersonalAccessToken: os.Getenv("AZDO_PAT"),
 39 | 		Project:             os.Getenv("AZURE_DEVOPS_PROJECT"),
 40 | 	}
 41 | 
 42 | 	// Validate configuration
 43 | 	if config.OrganizationURL == "" || config.PersonalAccessToken == "" || config.Project == "" {
 44 | 		log.Fatal("Missing required environment variables: AZURE_DEVOPS_ORG, AZDO_PAT, AZURE_DEVOPS_PROJECT")
 45 | 	}
 46 | 
 47 | 	// Initialize Azure DevOps clients
 48 | 	if err := initializeClients(config); err != nil {
 49 | 		log.Fatalf("Failed to initialize Azure DevOps clients: %v", err)
 50 | 	}
 51 | 
 52 | 	// Create MCP server
 53 | 	s := server.NewMCPServer(
 54 | 		"MCP Azure DevOps Bridge",
 55 | 		"1.0.0",
 56 | 		server.WithResourceCapabilities(false, false),
 57 | 		server.WithPromptCapabilities(true),
 58 | 		server.WithLogging(),
 59 | 	)
 60 | 
 61 | 	// Configure custom error handling
 62 | 	log.SetFlags(log.LstdFlags | log.Lshortfile)
 63 | 	log.SetOutput(&logWriter{})
 64 | 
 65 | 	// Add Work Item tools
 66 | 	addWorkItemTools(s)
 67 | 
 68 | 	// Add Wiki tools
 69 | 	addWikiTools(s)
 70 | 
 71 | 	// Start the server
 72 | 	if err := server.ServeStdio(s); err != nil {
 73 | 		log.Fatalf("Server error: %v\n", err)
 74 | 	}
 75 | }
 76 | 
 77 | func max(a, b int) int {
 78 | 	if a > b {
 79 | 		return a
 80 | 	}
 81 | 	return b
 82 | }
 83 | 
 84 | func min(a, b int) int {
 85 | 	if a < b {
 86 | 		return a
 87 | 	}
 88 | 	return b
 89 | }
 90 | 
 91 | func stringPtr(s string) *string {
 92 | 	return &s
 93 | }
 94 | 
 95 | // Initialize Azure DevOps clients
 96 | func initializeClients(config AzureDevOpsConfig) error {
 97 | 	connection = azuredevops.NewPatConnection(config.OrganizationURL, config.PersonalAccessToken)
 98 | 
 99 | 	ctx := context.Background()
100 | 
101 | 	var err error
102 | 
103 | 	// Initialize Work Item Tracking client
104 | 	workItemClient, err = workitemtracking.NewClient(ctx, connection)
105 | 	if err != nil {
106 | 		return fmt.Errorf("failed to create work item client: %v", err)
107 | 	}
108 | 
109 | 	// Initialize Wiki client
110 | 	wikiClient, err = wiki.NewClient(ctx, connection)
111 | 	if err != nil {
112 | 		return fmt.Errorf("failed to create wiki client: %v", err)
113 | 	}
114 | 
115 | 	// Initialize Core client
116 | 	coreClient, err = core.NewClient(ctx, connection)
117 | 	if err != nil {
118 | 		return fmt.Errorf("failed to create core client: %v", err)
119 | 	}
120 | 
121 | 	return nil
122 | }
123 | 
124 | type logWriter struct{}
125 | 
126 | func (w *logWriter) Write(bytes []byte) (int, error) {
127 | 	// Skip logging "Prompts not supported" errors
128 | 	if strings.Contains(string(bytes), "Prompts not supported") {
129 | 		return len(bytes), nil
130 | 	}
131 | 	return fmt.Print(string(bytes))
132 | }
133 | 
```

--------------------------------------------------------------------------------
/template.go:
--------------------------------------------------------------------------------

```go
  1 | package main
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"encoding/json"
  6 | 	"fmt"
  7 | 	"strings"
  8 | 
  9 | 	"github.com/google/uuid"
 10 | 	"github.com/mark3labs/mcp-go/mcp"
 11 | 	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/webapi"
 12 | 	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/workitemtracking"
 13 | )
 14 | 
 15 | // Handler for getting work item templates
 16 | func handleGetWorkItemTemplates(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 17 | 	workItemType := request.Params.Arguments["type"].(string)
 18 | 
 19 | 	templates, err := workItemClient.GetTemplates(ctx, workitemtracking.GetTemplatesArgs{
 20 | 		Project:          &config.Project,
 21 | 		Team:             nil, // Get templates for entire project
 22 | 		Workitemtypename: &workItemType,
 23 | 	})
 24 | 	if err != nil {
 25 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to get templates: %v", err)), nil
 26 | 	}
 27 | 
 28 | 	var results []string
 29 | 	for _, template := range *templates {
 30 | 		results = append(results, fmt.Sprintf("Template ID: %s\nName: %s\nDescription: %s\n---",
 31 | 			*template.Id,
 32 | 			*template.Name,
 33 | 			*template.Description))
 34 | 	}
 35 | 
 36 | 	if len(results) == 0 {
 37 | 		return mcp.NewToolResultText(fmt.Sprintf("No templates found for type: %s", workItemType)), nil
 38 | 	}
 39 | 
 40 | 	return mcp.NewToolResultText(strings.Join(results, "\n")), nil
 41 | }
 42 | 
 43 | // Handler for creating work item from template
 44 | func handleCreateFromTemplate(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 45 | 	templateID := request.Params.Arguments["template_id"].(string)
 46 | 	fieldValuesJSON := request.Params.Arguments["field_values"].(string)
 47 | 
 48 | 	var fieldValues map[string]interface{}
 49 | 	if err := json.Unmarshal([]byte(fieldValuesJSON), &fieldValues); err != nil {
 50 | 		return mcp.NewToolResultError(fmt.Sprintf("Invalid field values JSON: %v", err)), nil
 51 | 	}
 52 | 
 53 | 	// Convert template ID to UUID
 54 | 	templateUUID, err := uuid.Parse(templateID)
 55 | 	if err != nil {
 56 | 		return mcp.NewToolResultError(fmt.Sprintf("Invalid template ID format: %v", err)), nil
 57 | 	}
 58 | 
 59 | 	// Get template
 60 | 	template, err := workItemClient.GetTemplate(ctx, workitemtracking.GetTemplateArgs{
 61 | 		Project:    &config.Project,
 62 | 		Team:       nil,
 63 | 		TemplateId: &templateUUID,
 64 | 	})
 65 | 	if err != nil {
 66 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to get template: %v", err)), nil
 67 | 	}
 68 | 
 69 | 	// Create work item from template
 70 | 	createArgs := workitemtracking.CreateWorkItemArgs{
 71 | 		Type:    template.WorkItemTypeName,
 72 | 		Project: &config.Project,
 73 | 	}
 74 | 
 75 | 	// Add template fields
 76 | 	var operations []webapi.JsonPatchOperation
 77 | 	for field, value := range *template.Fields {
 78 | 		operations = append(operations, webapi.JsonPatchOperation{
 79 | 			Op:    &webapi.OperationValues.Add,
 80 | 			Path:  stringPtr("/fields/" + field),
 81 | 			Value: value,
 82 | 		})
 83 | 	}
 84 | 
 85 | 	// Override with provided field values
 86 | 	for field, value := range fieldValues {
 87 | 		operations = append(operations, webapi.JsonPatchOperation{
 88 | 			Op:    &webapi.OperationValues.Add,
 89 | 			Path:  stringPtr("/fields/" + field),
 90 | 			Value: value,
 91 | 		})
 92 | 	}
 93 | 
 94 | 	createArgs.Document = &operations
 95 | 
 96 | 	workItem, err := workItemClient.CreateWorkItem(ctx, createArgs)
 97 | 	if err != nil {
 98 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to create work item from template: %v", err)), nil
 99 | 	}
100 | 
101 | 	return mcp.NewToolResultText(fmt.Sprintf("Created work item #%d from template", *workItem.Id)), nil
102 | }
103 | 
```

--------------------------------------------------------------------------------
/sprint.go:
--------------------------------------------------------------------------------

```go
  1 | package main
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"encoding/json"
  6 | 	"fmt"
  7 | 	"net/http"
  8 | 	"net/url"
  9 | 	"strings"
 10 | 	"time"
 11 | 
 12 | 	"github.com/mark3labs/mcp-go/mcp"
 13 | )
 14 | 
 15 | func handleGetCurrentSprint(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 16 | 	team, _ := request.Params.Arguments["team"].(string)
 17 | 	if team == "" {
 18 | 		team = config.Project + " Team" // Default team name
 19 | 	}
 20 | 
 21 | 	// Build the URL for the current iteration
 22 | 	baseURL := fmt.Sprintf("%s/%s/_apis/work/teamsettings/iterations",
 23 | 		config.OrganizationURL,
 24 | 		config.Project)
 25 | 
 26 | 	queryParams := url.Values{}
 27 | 	queryParams.Add("$timeframe", "current")
 28 | 	queryParams.Add("api-version", "7.2-preview")
 29 | 
 30 | 	fullURL := fmt.Sprintf("%s?%s", baseURL, queryParams.Encode())
 31 | 
 32 | 	// Create request
 33 | 	req, err := http.NewRequest("GET", fullURL, nil)
 34 | 	if err != nil {
 35 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to create request: %v", err)), nil
 36 | 	}
 37 | 
 38 | 	// Add authentication
 39 | 	req.SetBasicAuth("", config.PersonalAccessToken)
 40 | 
 41 | 	// Send request
 42 | 	client := &http.Client{}
 43 | 	resp, err := client.Do(req)
 44 | 	if err != nil {
 45 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to get current sprint: %v", err)), nil
 46 | 	}
 47 | 	defer resp.Body.Close()
 48 | 
 49 | 	if resp.StatusCode != http.StatusOK {
 50 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to get current sprint. Status: %d", resp.StatusCode)), nil
 51 | 	}
 52 | 
 53 | 	// Parse response
 54 | 	var sprintResponse struct {
 55 | 		Value []struct {
 56 | 			Name      string    `json:"name"`
 57 | 			StartDate time.Time `json:"startDate"`
 58 | 			EndDate   time.Time `json:"finishDate"`
 59 | 		} `json:"value"`
 60 | 	}
 61 | 
 62 | 	if err := json.NewDecoder(resp.Body).Decode(&sprintResponse); err != nil {
 63 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to parse response: %v", err)), nil
 64 | 	}
 65 | 
 66 | 	if len(sprintResponse.Value) == 0 {
 67 | 		return mcp.NewToolResultText("No active sprint found"), nil
 68 | 	}
 69 | 
 70 | 	sprint := sprintResponse.Value[0]
 71 | 	result := fmt.Sprintf("Current Sprint: %s\nStart Date: %s\nEnd Date: %s",
 72 | 		sprint.Name,
 73 | 		sprint.StartDate.Format("2006-01-02"),
 74 | 		sprint.EndDate.Format("2006-01-02"))
 75 | 
 76 | 	return mcp.NewToolResultText(result), nil
 77 | }
 78 | 
 79 | func handleGetSprints(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 80 | 	team, _ := request.Params.Arguments["team"].(string)
 81 | 	includeCompleted, _ := request.Params.Arguments["include_completed"].(bool)
 82 | 	if team == "" {
 83 | 		team = config.Project + " Team"
 84 | 	}
 85 | 
 86 | 	// Build the URL for iterations
 87 | 	baseURL := fmt.Sprintf("%s/%s/_apis/work/teamsettings/iterations",
 88 | 		config.OrganizationURL,
 89 | 		config.Project)
 90 | 
 91 | 	queryParams := url.Values{}
 92 | 	if !includeCompleted {
 93 | 		queryParams.Add("$timeframe", "current,future")
 94 | 	}
 95 | 	queryParams.Add("api-version", "7.2-preview")
 96 | 
 97 | 	fullURL := fmt.Sprintf("%s?%s", baseURL, queryParams.Encode())
 98 | 
 99 | 	req, err := http.NewRequest("GET", fullURL, nil)
100 | 	if err != nil {
101 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to create request: %v", err)), nil
102 | 	}
103 | 
104 | 	req.SetBasicAuth("", config.PersonalAccessToken)
105 | 
106 | 	client := &http.Client{}
107 | 	resp, err := client.Do(req)
108 | 	if err != nil {
109 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to get sprints: %v", err)), nil
110 | 	}
111 | 	defer resp.Body.Close()
112 | 
113 | 	if resp.StatusCode != http.StatusOK {
114 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to get sprints. Status: %d", resp.StatusCode)), nil
115 | 	}
116 | 
117 | 	var sprintResponse struct {
118 | 		Value []struct {
119 | 			Name      string    `json:"name"`
120 | 			StartDate time.Time `json:"startDate"`
121 | 			EndDate   time.Time `json:"finishDate"`
122 | 		} `json:"value"`
123 | 	}
124 | 
125 | 	if err := json.NewDecoder(resp.Body).Decode(&sprintResponse); err != nil {
126 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to parse response: %v", err)), nil
127 | 	}
128 | 
129 | 	var results []string
130 | 	for _, sprint := range sprintResponse.Value {
131 | 		results = append(results, fmt.Sprintf("Sprint: %s\nStart: %s\nEnd: %s\n---",
132 | 			sprint.Name,
133 | 			sprint.StartDate.Format("2006-01-02"),
134 | 			sprint.EndDate.Format("2006-01-02")))
135 | 	}
136 | 
137 | 	if len(results) == 0 {
138 | 		return mcp.NewToolResultText("No sprints found"), nil
139 | 	}
140 | 
141 | 	return mcp.NewToolResultText(strings.Join(results, "\n")), nil
142 | }
143 | 
```

--------------------------------------------------------------------------------
/attachment.go:
--------------------------------------------------------------------------------

```go
  1 | package main
  2 | 
  3 | import (
  4 | 	"bytes"
  5 | 	"context"
  6 | 	"encoding/base64"
  7 | 	"fmt"
  8 | 	"strings"
  9 | 
 10 | 	"github.com/mark3labs/mcp-go/mcp"
 11 | 	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/webapi"
 12 | 	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/workitemtracking"
 13 | )
 14 | 
 15 | // Handler for adding attachment to work item
 16 | func handleAddWorkItemAttachment(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 17 | 	id := int(request.Params.Arguments["id"].(float64))
 18 | 	fileName := request.Params.Arguments["file_name"].(string)
 19 | 	content := request.Params.Arguments["content"].(string)
 20 | 
 21 | 	// Decode base64 content
 22 | 	fileContent, err := base64.StdEncoding.DecodeString(content)
 23 | 	if err != nil {
 24 | 		return mcp.NewToolResultError(fmt.Sprintf("Invalid base64 content: %v", err)), nil
 25 | 	}
 26 | 
 27 | 	// Create upload stream
 28 | 	stream := bytes.NewReader(fileContent)
 29 | 
 30 | 	// Upload attachment
 31 | 	attachment, err := workItemClient.CreateAttachment(ctx, workitemtracking.CreateAttachmentArgs{
 32 | 		UploadStream: stream,
 33 | 		FileName:     &fileName,
 34 | 		Project:      &config.Project,
 35 | 	})
 36 | 	if err != nil {
 37 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to upload attachment: %v", err)), nil
 38 | 	}
 39 | 
 40 | 	// Add attachment reference to work item
 41 | 	updateArgs := workitemtracking.UpdateWorkItemArgs{
 42 | 		Id:      &id,
 43 | 		Project: &config.Project,
 44 | 		Document: &[]webapi.JsonPatchOperation{
 45 | 			{
 46 | 				Op:   &webapi.OperationValues.Add,
 47 | 				Path: stringPtr("/relations/-"),
 48 | 				Value: map[string]interface{}{
 49 | 					"rel": "AttachedFile",
 50 | 					"url": *attachment.Url,
 51 | 					"attributes": map[string]interface{}{
 52 | 						"name": fileName,
 53 | 					},
 54 | 				},
 55 | 			},
 56 | 		},
 57 | 	}
 58 | 
 59 | 	_, err = workItemClient.UpdateWorkItem(ctx, updateArgs)
 60 | 	if err != nil {
 61 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to add attachment to work item: %v", err)), nil
 62 | 	}
 63 | 
 64 | 	return mcp.NewToolResultText(fmt.Sprintf("Added attachment '%s' to work item #%d", fileName, id)), nil
 65 | }
 66 | 
 67 | // Handler for getting work item attachments
 68 | func handleGetWorkItemAttachments(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 69 | 	id := int(request.Params.Arguments["id"].(float64))
 70 | 
 71 | 	workItem, err := workItemClient.GetWorkItem(ctx, workitemtracking.GetWorkItemArgs{
 72 | 		Id:      &id,
 73 | 		Project: &config.Project,
 74 | 		Expand:  &workitemtracking.WorkItemExpandValues.Relations,
 75 | 	})
 76 | 	if err != nil {
 77 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to get work item: %v", err)), nil
 78 | 	}
 79 | 
 80 | 	if workItem.Relations == nil {
 81 | 		return mcp.NewToolResultText(fmt.Sprintf("No attachments found for work item #%d", id)), nil
 82 | 	}
 83 | 
 84 | 	var results []string
 85 | 	for _, relation := range *workItem.Relations {
 86 | 		if *relation.Rel == "AttachedFile" {
 87 | 			name := (*relation.Attributes)["name"].(string)
 88 | 			results = append(results, fmt.Sprintf("ID: %s\nName: %s\nURL: %s\n---",
 89 | 				*relation.Url,
 90 | 				name,
 91 | 				*relation.Url))
 92 | 		}
 93 | 	}
 94 | 
 95 | 	if len(results) == 0 {
 96 | 		return mcp.NewToolResultText(fmt.Sprintf("No attachments found for work item #%d", id)), nil
 97 | 	}
 98 | 
 99 | 	return mcp.NewToolResultText(strings.Join(results, "\n")), nil
100 | }
101 | 
102 | // Handler for removing attachment from work item
103 | func handleRemoveWorkItemAttachment(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
104 | 	id := int(request.Params.Arguments["id"].(float64))
105 | 	attachmentID := request.Params.Arguments["attachment_id"].(string)
106 | 
107 | 	workItem, err := workItemClient.GetWorkItem(ctx, workitemtracking.GetWorkItemArgs{
108 | 		Id:      &id,
109 | 		Project: &config.Project,
110 | 		Expand:  &workitemtracking.WorkItemExpandValues.Relations,
111 | 	})
112 | 	if err != nil {
113 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to get work item: %v", err)), nil
114 | 	}
115 | 
116 | 	if workItem.Relations == nil {
117 | 		return mcp.NewToolResultError("Work item has no attachments"), nil
118 | 	}
119 | 
120 | 	// Find the attachment relation index
121 | 	var relationIndex int = -1
122 | 	for i, relation := range *workItem.Relations {
123 | 		if *relation.Rel == "AttachedFile" && strings.Contains(*relation.Url, attachmentID) {
124 | 			relationIndex = i
125 | 			break
126 | 		}
127 | 	}
128 | 
129 | 	if relationIndex == -1 {
130 | 		return mcp.NewToolResultError("Attachment not found"), nil
131 | 	}
132 | 
133 | 	// Remove the attachment relation
134 | 	updateArgs := workitemtracking.UpdateWorkItemArgs{
135 | 		Id:      &id,
136 | 		Project: &config.Project,
137 | 		Document: &[]webapi.JsonPatchOperation{
138 | 			{
139 | 				Op:   &webapi.OperationValues.Remove,
140 | 				Path: stringPtr(fmt.Sprintf("/relations/%d", relationIndex)),
141 | 			},
142 | 		},
143 | 	}
144 | 
145 | 	_, err = workItemClient.UpdateWorkItem(ctx, updateArgs)
146 | 	if err != nil {
147 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to remove attachment: %v", err)), nil
148 | 	}
149 | 
150 | 	return mcp.NewToolResultText(fmt.Sprintf("Removed attachment from work item #%d", id)), nil
151 | }
152 | 
```

--------------------------------------------------------------------------------
/wiki.go:
--------------------------------------------------------------------------------

```go
  1 | package main
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"encoding/json"
  6 | 	"fmt"
  7 | 	"io"
  8 | 	"log"
  9 | 	"net/http"
 10 | 	"net/url"
 11 | 	"strings"
 12 | 
 13 | 	"github.com/mark3labs/mcp-go/mcp"
 14 | 	"github.com/mark3labs/mcp-go/server"
 15 | 	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/wiki"
 16 | )
 17 | 
 18 | func addWikiTools(s *server.MCPServer) {
 19 | 	// Wiki Page Management
 20 | 	manageWikiTool := mcp.NewTool("manage_wiki_page",
 21 | 		mcp.WithDescription("Create or update a wiki page"),
 22 | 		mcp.WithString("path",
 23 | 			mcp.Required(),
 24 | 			mcp.Description("Path of the wiki page"),
 25 | 		),
 26 | 		mcp.WithString("content",
 27 | 			mcp.Required(),
 28 | 			mcp.Description("Content of the wiki page in markdown format"),
 29 | 		),
 30 | 	)
 31 | 	s.AddTool(manageWikiTool, handleManageWikiPage)
 32 | 
 33 | 	// Get Wiki Page
 34 | 	getWikiTool := mcp.NewTool("get_wiki_page",
 35 | 		mcp.WithDescription("Get content of a wiki page"),
 36 | 		mcp.WithString("path",
 37 | 			mcp.Required(),
 38 | 			mcp.Description("Path of the wiki page to retrieve"),
 39 | 		),
 40 | 		mcp.WithBoolean("include_children",
 41 | 			mcp.Description("Whether to include child pages"),
 42 | 		),
 43 | 	)
 44 | 	s.AddTool(getWikiTool, handleGetWikiPage)
 45 | 
 46 | 	// List Wiki Pages
 47 | 	listWikiTool := mcp.NewTool("list_wiki_pages",
 48 | 		mcp.WithDescription("List wiki pages in a directory"),
 49 | 		mcp.WithString("path",
 50 | 			mcp.Description("Path to list pages from (optional)"),
 51 | 		),
 52 | 		mcp.WithBoolean("recursive",
 53 | 			mcp.Description("Whether to list pages recursively"),
 54 | 		),
 55 | 	)
 56 | 	s.AddTool(listWikiTool, handleListWikiPages)
 57 | 
 58 | 	// Search Wiki
 59 | 	searchWikiTool := mcp.NewTool("search_wiki",
 60 | 		mcp.WithDescription("Search wiki pages"),
 61 | 		mcp.WithString("query",
 62 | 			mcp.Required(),
 63 | 			mcp.Description("Search query"),
 64 | 		),
 65 | 		mcp.WithString("path",
 66 | 			mcp.Description("Path to limit search to (optional)"),
 67 | 		),
 68 | 	)
 69 | 	s.AddTool(searchWikiTool, handleSearchWiki)
 70 | 
 71 | 	// Get Available Wikis
 72 | 	getWikisTool := mcp.NewTool("get_available_wikis",
 73 | 		mcp.WithDescription("Get information about available wikis"),
 74 | 	)
 75 | 	s.AddTool(getWikisTool, handleGetWikis)
 76 | }
 77 | 
 78 | func handleManageWikiPage(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 79 | 	path := request.Params.Arguments["path"].(string)
 80 | 	content := request.Params.Arguments["content"].(string)
 81 | 	// Note: Comments are not supported by the Azure DevOps Wiki API
 82 | 	_, _ = request.Params.Arguments["comment"].(string)
 83 | 
 84 | 	// Get all available wikis for the project
 85 | 	wikis, err := getWikisForProject(ctx)
 86 | 	if err != nil {
 87 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to get wikis: %v", err)), nil
 88 | 	}
 89 | 
 90 | 	if len(wikis) == 0 {
 91 | 		return mcp.NewToolResultError("No wikis found for this project"), nil
 92 | 	}
 93 | 
 94 | 	// Use the first wiki by default, or try to match by project name
 95 | 	wikiId := *wikis[0].Id
 96 | 	for _, wiki := range wikis {
 97 | 		if strings.Contains(*wiki.Name, config.Project) {
 98 | 			wikiId = *wiki.Id
 99 | 			break
100 | 		}
101 | 	}
102 | 
103 | 	// Convert wiki ID to the format expected by the API
104 | 	wikiIdentifier := fmt.Sprintf("%s", wikiId)
105 | 
106 | 	_, err = wikiClient.CreateOrUpdatePage(ctx, wiki.CreateOrUpdatePageArgs{
107 | 		WikiIdentifier: &wikiIdentifier,
108 | 		Path:           &path,
109 | 		Project:        &config.Project,
110 | 		Parameters: &wiki.WikiPageCreateOrUpdateParameters{
111 | 			Content: &content,
112 | 		},
113 | 	})
114 | 
115 | 	if err != nil {
116 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to manage wiki page: %v", err)), nil
117 | 	}
118 | 
119 | 	return mcp.NewToolResultText(fmt.Sprintf("Successfully managed wiki page: %s", path)), nil
120 | }
121 | 
122 | func handleGetWikiPage(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
123 | 	path := request.Params.Arguments["path"].(string)
124 | 	includeChildren, _ := request.Params.Arguments["include_children"].(bool)
125 | 
126 | 	// Ensure path starts with a forward slash
127 | 	if !strings.HasPrefix(path, "/") {
128 | 		path = "/" + path
129 | 	}
130 | 
131 | 	log.Printf("Wiki page path: %s", path)
132 | 
133 | 	recursionLevel := "none"
134 | 	if includeChildren {
135 | 		recursionLevel = "oneLevel"
136 | 	}
137 | 
138 | 	// Get all available wikis for the project
139 | 	wikis, err := getWikisForProject(ctx)
140 | 	if err != nil {
141 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to get wikis: %v", err)), nil
142 | 	}
143 | 
144 | 	log.Printf("Found %d wikis for project", len(wikis))
145 | 	for i, wiki := range wikis {
146 | 		log.Printf("Wiki %d: %s (ID: %s)", i+1, *wiki.Name, *wiki.Id)
147 | 	}
148 | 
149 | 	if len(wikis) == 0 {
150 | 		return mcp.NewToolResultError("No wikis found for this project"), nil
151 | 	}
152 | 
153 | 	// Use the first wiki by default
154 | 	wikiId := *wikis[0].Id
155 | 	
156 | 	// Try to find a wiki with a name that matches or contains the project name
157 | 	projectName := strings.Replace(config.Project, " ", "", -1)
158 | 	projectName = strings.ToLower(projectName)
159 | 	
160 | 	for _, wiki := range wikis {
161 | 		wikiName := strings.ToLower(*wiki.Name)
162 | 		if strings.Contains(wikiName, projectName) || strings.Contains(wikiName, "documentation") {
163 | 			wikiId = *wiki.Id
164 | 			log.Printf("Selected wiki: %s (ID: %s)", *wiki.Name, wikiId)
165 | 			break
166 | 		}
167 | 	}
168 | 
169 | 	// Build the URL with query parameters
170 | 	baseURL := fmt.Sprintf("%s/%s/_apis/wiki/wikis/%s/pages",
171 | 		config.OrganizationURL,
172 | 		url.PathEscape(config.Project),
173 | 		wikiId)
174 | 
175 | 	queryParams := url.Values{}
176 | 	queryParams.Add("path", path)
177 | 	queryParams.Add("recursionLevel", recursionLevel)
178 | 	queryParams.Add("includeContent", "true")
179 | 	queryParams.Add("api-version", "7.2-preview")
180 | 
181 | 	fullURL := fmt.Sprintf("%s?%s", baseURL, queryParams.Encode())
182 | 	log.Printf("Requesting wiki page from URL: %s", fullURL)
183 | 
184 | 	// Create request
185 | 	req, err := http.NewRequest("GET", fullURL, nil)
186 | 	if err != nil {
187 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to create request: %v", err)), nil
188 | 	}
189 | 
190 | 	// Add authentication
191 | 	req.SetBasicAuth("", config.PersonalAccessToken)
192 | 
193 | 	// Send request
194 | 	client := &http.Client{}
195 | 	resp, err := client.Do(req)
196 | 	if err != nil {
197 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to get wiki page: %v", err)), nil
198 | 	}
199 | 	defer resp.Body.Close()
200 | 
201 | 	// Read the response body
202 | 	responseBody, err := io.ReadAll(resp.Body)
203 | 	if err != nil {
204 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to read response body: %v", err)), nil
205 | 	}
206 | 
207 | 	if resp.StatusCode != http.StatusOK {
208 | 		// Log more details about the error
209 | 		log.Printf("Wiki API Error - Status: %d, Response: %s", resp.StatusCode, string(responseBody))
210 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to get wiki page. Status: %d", resp.StatusCode)), nil
211 | 	}
212 | 
213 | 	// Parse response
214 | 	var wikiResponse struct {
215 | 		Content  string `json:"content"`
216 | 		SubPages []struct {
217 | 			Path    string `json:"path"`
218 | 			Content string `json:"content"`
219 | 		} `json:"subPages"`
220 | 	}
221 | 
222 | 	log.Printf("Wiki API Response: %s", string(responseBody))
223 | 	
224 | 	if err := json.Unmarshal(responseBody, &wikiResponse); err != nil {
225 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to parse response: %v", err)), nil
226 | 	}
227 | 
228 | 	// Format result
229 | 	var result strings.Builder
230 | 	result.WriteString(fmt.Sprintf("=== %s ===\n\n", path))
231 | 	result.WriteString(wikiResponse.Content)
232 | 
233 | 	if includeChildren && len(wikiResponse.SubPages) > 0 {
234 | 		result.WriteString("\n\nSub-pages:\n")
235 | 		for _, subPage := range wikiResponse.SubPages {
236 | 			result.WriteString(fmt.Sprintf("\n=== %s ===\n", subPage.Path))
237 | 			result.WriteString(subPage.Content)
238 | 			result.WriteString("\n")
239 | 		}
240 | 	}
241 | 
242 | 	return mcp.NewToolResultText(result.String()), nil
243 | }
244 | 
245 | func handleListWikiPages(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
246 | 	path, _ := request.Params.Arguments["path"].(string)
247 | 	recursive, _ := request.Params.Arguments["recursive"].(bool)
248 | 
249 | 	recursionLevel := "oneLevel"
250 | 	if recursive {
251 | 		recursionLevel = "full"
252 | 	}
253 | 
254 | 	// Get all available wikis for the project
255 | 	wikis, err := getWikisForProject(ctx)
256 | 	if err != nil {
257 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to get wikis: %v", err)), nil
258 | 	}
259 | 
260 | 	if len(wikis) == 0 {
261 | 		return mcp.NewToolResultError("No wikis found for this project"), nil
262 | 	}
263 | 
264 | 	// Use the first wiki by default, or try to match by project name
265 | 	wikiId := *wikis[0].Id
266 | 	for _, wiki := range wikis {
267 | 		if strings.Contains(*wiki.Name, config.Project) {
268 | 			wikiId = *wiki.Id
269 | 			break
270 | 		}
271 | 	}
272 | 
273 | 	// Build the URL with query parameters
274 | 	baseURL := fmt.Sprintf("%s/%s/_apis/wiki/wikis/%s/pages",
275 | 		config.OrganizationURL,
276 | 		url.PathEscape(config.Project),
277 | 		wikiId)
278 | 
279 | 	queryParams := url.Values{}
280 | 	if path != "" {
281 | 		queryParams.Add("path", path)
282 | 	}
283 | 	queryParams.Add("recursionLevel", recursionLevel)
284 | 	queryParams.Add("api-version", "7.2-preview")
285 | 
286 | 	fullURL := fmt.Sprintf("%s?%s", baseURL, queryParams.Encode())
287 | 
288 | 	// Create request
289 | 	req, err := http.NewRequest("GET", fullURL, nil)
290 | 	if err != nil {
291 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to create request: %v", err)), nil
292 | 	}
293 | 
294 | 	// Add authentication
295 | 	req.SetBasicAuth("", config.PersonalAccessToken)
296 | 
297 | 	// Send request
298 | 	client := &http.Client{}
299 | 	resp, err := client.Do(req)
300 | 	if err != nil {
301 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to list wiki pages: %v", err)), nil
302 | 	}
303 | 	defer resp.Body.Close()
304 | 
305 | 	// Read the response body
306 | 	responseBody, err := io.ReadAll(resp.Body)
307 | 	if err != nil {
308 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to read response body: %v", err)), nil
309 | 	}
310 | 
311 | 	if resp.StatusCode != http.StatusOK {
312 | 		// Log error details
313 | 		log.Printf("Wiki API Error - Status: %d, Response: %s", resp.StatusCode, string(responseBody))
314 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to list wiki pages. Status: %d", resp.StatusCode)), nil
315 | 	}
316 | 
317 | 	// Parse response
318 | 	var listResponse struct {
319 | 		Value []struct {
320 | 			Path       string `json:"path"`
321 | 			RemotePath string `json:"remotePath"`
322 | 			IsFolder   bool   `json:"isFolder"`
323 | 		} `json:"value"`
324 | 	}
325 | 
326 | 	log.Printf("Wiki API Response: %s", string(responseBody))
327 | 	
328 | 	if err := json.Unmarshal(responseBody, &listResponse); err != nil {
329 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to parse response: %v", err)), nil
330 | 	}
331 | 
332 | 	// Format result
333 | 	var result strings.Builder
334 | 	var locationText string
335 | 	if path != "" {
336 | 		locationText = " in " + path
337 | 	}
338 | 	result.WriteString(fmt.Sprintf("Wiki pages%s:\n\n", locationText))
339 | 
340 | 	for _, item := range listResponse.Value {
341 | 		prefix := "📄 "
342 | 		if item.IsFolder {
343 | 			prefix = "📁 "
344 | 		}
345 | 		result.WriteString(fmt.Sprintf("%s%s\n", prefix, item.Path))
346 | 	}
347 | 
348 | 	return mcp.NewToolResultText(result.String()), nil
349 | }
350 | 
351 | func handleSearchWiki(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
352 | 	query := request.Params.Arguments["query"].(string)
353 | 	path, hasPath := request.Params.Arguments["path"].(string)
354 | 
355 | 	// Get all available wikis for the project
356 | 	wikis, err := getWikisForProject(ctx)
357 | 	if err != nil {
358 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to get wikis: %v", err)), nil
359 | 	}
360 | 
361 | 	if len(wikis) == 0 {
362 | 		return mcp.NewToolResultError("No wikis found for this project"), nil
363 | 	}
364 | 
365 | 	// Use the first wiki by default, or try to match by project name
366 | 	wikiId := *wikis[0].Id
367 | 	for _, wiki := range wikis {
368 | 		if strings.Contains(*wiki.Name, config.Project) {
369 | 			wikiId = *wiki.Id
370 | 			break
371 | 		}
372 | 	}
373 | 
374 | 	// First, get all pages (potentially under the specified path)
375 | 	baseURL := fmt.Sprintf("%s/%s/_apis/wiki/wikis/%s/pages",
376 | 		config.OrganizationURL,
377 | 		url.PathEscape(config.Project),
378 | 		wikiId)
379 | 
380 | 	queryParams := url.Values{}
381 | 	queryParams.Add("recursionLevel", "full")
382 | 	if hasPath {
383 | 		queryParams.Add("path", path)
384 | 	}
385 | 	queryParams.Add("includeContent", "true")
386 | 	queryParams.Add("api-version", "7.2-preview")
387 | 
388 | 	fullURL := fmt.Sprintf("%s?%s", baseURL, queryParams.Encode())
389 | 
390 | 	// Create request
391 | 	req, err := http.NewRequest("GET", fullURL, nil)
392 | 	if err != nil {
393 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to create request: %v", err)), nil
394 | 	}
395 | 
396 | 	// Add authentication
397 | 	req.SetBasicAuth("", config.PersonalAccessToken)
398 | 
399 | 	// Send request
400 | 	client := &http.Client{}
401 | 	resp, err := client.Do(req)
402 | 	if err != nil {
403 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to search wiki: %v", err)), nil
404 | 	}
405 | 	defer resp.Body.Close()
406 | 
407 | 	// Read the response body
408 | 	responseBody, err := io.ReadAll(resp.Body)
409 | 	if err != nil {
410 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to read response body: %v", err)), nil
411 | 	}
412 | 
413 | 	if resp.StatusCode != http.StatusOK {
414 | 		// Log error details
415 | 		log.Printf("Wiki API Error - Status: %d, Response: %s", resp.StatusCode, string(responseBody))
416 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to search wiki. Status: %d", resp.StatusCode)), nil
417 | 	}
418 | 
419 | 	// Parse response
420 | 	var searchResponse struct {
421 | 		Count int `json:"count"`
422 | 		Results []struct {
423 | 			FileName    string `json:"fileName"`
424 | 			Path        string `json:"path"`
425 | 			MatchCount  int    `json:"hitCount"`
426 | 			Repository  struct {
427 | 				ID string `json:"id"`
428 | 			} `json:"repository"`
429 | 			Hits []struct {
430 | 				Content    string `json:"content"`
431 | 				LineNumber int    `json:"startLine"`
432 | 			} `json:"hits"`
433 | 		} `json:"results"`
434 | 	}
435 | 
436 | 	log.Printf("Wiki API Search Response: %s", string(responseBody))
437 | 	
438 | 	if err := json.Unmarshal(responseBody, &searchResponse); err != nil {
439 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to parse response: %v", err)), nil
440 | 	}
441 | 
442 | 	// Search through the pages
443 | 	var results []string
444 | 	queryLower := strings.ToLower(query)
445 | 	for _, page := range searchResponse.Results {
446 | 		if strings.Contains(strings.ToLower(page.FileName), queryLower) {
447 | 			// Extract a snippet of context around the match
448 | 			contentLower := strings.ToLower(page.FileName)
449 | 			index := strings.Index(contentLower, queryLower)
450 | 			start := 0
451 | 			if index > 100 {
452 | 				start = index - 100
453 | 			}
454 | 			end := len(page.FileName)
455 | 			if index+len(query)+100 < len(page.FileName) {
456 | 				end = index + len(query) + 100
457 | 			}
458 | 
459 | 			snippet := page.FileName[start:end]
460 | 			if start > 0 {
461 | 				snippet = "..." + snippet
462 | 			}
463 | 			if end < len(page.FileName) {
464 | 				snippet = snippet + "..."
465 | 			}
466 | 
467 | 			results = append(results, fmt.Sprintf("Page: %s\nMatch: %s\n---\n", page.Path, snippet))
468 | 		}
469 | 	}
470 | 
471 | 	if len(results) == 0 {
472 | 		return mcp.NewToolResultText(fmt.Sprintf("No matches found for '%s'", query)), nil
473 | 	}
474 | 
475 | 	return mcp.NewToolResultText(fmt.Sprintf("Found %d matches:\n\n%s", len(results), strings.Join(results, "\n"))), nil
476 | }
477 | 
478 | func handleGetWikis(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
479 | 	wikis, err := getWikisForProject(ctx)
480 | 	if err != nil {
481 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to get wikis: %v", err)), nil
482 | 	}
483 | 
484 | 	if len(wikis) == 0 {
485 | 		return mcp.NewToolResultError("No wikis found for this project"), nil
486 | 	}
487 | 
488 | 	var result strings.Builder
489 | 	result.WriteString(fmt.Sprintf("Found %d wikis for project %s:\n\n", len(wikis), config.Project))
490 | 
491 | 	for i, wiki := range wikis {
492 | 		result.WriteString(fmt.Sprintf("%d. Wiki Name: %s\n   Wiki ID: %s\n\n",
493 | 			i+1, *wiki.Name, *wiki.Id))
494 | 	}
495 | 
496 | 	return mcp.NewToolResultText(result.String()), nil
497 | }
498 | 
499 | func getWikisForProject(ctx context.Context) ([]*wiki.Wiki, error) {
500 | 	// Create request
501 | 	wikiApiUrl := fmt.Sprintf("%s/%s/_apis/wiki/wikis?api-version=7.2-preview", 
502 | 		config.OrganizationURL,
503 | 		url.PathEscape(config.Project))
504 | 	log.Printf("Getting wikis from URL: %s", wikiApiUrl)
505 | 	
506 | 	req, err := http.NewRequest("GET", wikiApiUrl, nil)
507 | 	if err != nil {
508 | 		return nil, err
509 | 	}
510 | 
511 | 	// Add authentication
512 | 	req.SetBasicAuth("", config.PersonalAccessToken)
513 | 
514 | 	// Send request
515 | 	client := &http.Client{}
516 | 	resp, err := client.Do(req)
517 | 	if err != nil {
518 | 		return nil, err
519 | 	}
520 | 	defer resp.Body.Close()
521 | 
522 | 	log.Printf("Wiki API Status Code: %d", resp.StatusCode)
523 | 	
524 | 	// Read the response body
525 | 	bodyBytes, err := io.ReadAll(resp.Body)
526 | 	if err != nil {
527 | 		return nil, fmt.Errorf("Failed to read response body: %v", err)
528 | 	}
529 | 	
530 | 	if resp.StatusCode != http.StatusOK {
531 | 		log.Printf("Error response: %s", string(bodyBytes))
532 | 		return nil, fmt.Errorf("Failed to get wikis. Status: %d", resp.StatusCode)
533 | 	}
534 | 
535 | 	// Parse response
536 | 	var wikisResponse struct {
537 | 		Value []*wiki.Wiki `json:"value"`
538 | 	}
539 | 	
540 | 	log.Printf("Wiki API Response: %s", string(bodyBytes))
541 | 	
542 | 	// Unmarshal JSON directly from the bytes
543 | 	if err := json.Unmarshal(bodyBytes, &wikisResponse); err != nil {
544 | 		return nil, fmt.Errorf("Failed to parse wikis response: %v", err)
545 | 	}
546 | 
547 | 	log.Printf("Found %d wikis in total", len(wikisResponse.Value))
548 | 	
549 | 	// For now, return all wikis since we don't have a reliable way to filter
550 | 	// If needed, we can add more specific filtering later
551 | 	if len(wikisResponse.Value) > 0 {
552 | 		log.Printf("First wiki: Name=%s, ID=%s", *wikisResponse.Value[0].Name, *wikisResponse.Value[0].Id)
553 | 	}
554 | 	
555 | 	return wikisResponse.Value, nil
556 | }
557 | 
```

--------------------------------------------------------------------------------
/workitems.go:
--------------------------------------------------------------------------------

```go
  1 | package main
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"encoding/json"
  6 | 	"fmt"
  7 | 	"strconv"
  8 | 	"strings"
  9 | 
 10 | 	"github.com/mark3labs/mcp-go/mcp"
 11 | 	"github.com/mark3labs/mcp-go/server"
 12 | 	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/webapi"
 13 | 	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/workitemtracking"
 14 | )
 15 | 
 16 | func addWorkItemTools(s *server.MCPServer) {
 17 | 	// Add WIQL Query Format Prompt
 18 | 	s.AddPrompt(mcp.NewPrompt("wiql_query_format",
 19 | 		mcp.WithPromptDescription("Helper for formatting WIQL queries for common scenarios"),
 20 | 		mcp.WithArgument("query_type",
 21 | 			mcp.ArgumentDescription("Type of query to format (current_sprint, assigned_to_me, etc)"),
 22 | 			mcp.RequiredArgument(),
 23 | 		),
 24 | 		mcp.WithArgument("additional_fields",
 25 | 			mcp.ArgumentDescription("Additional fields to include in the SELECT clause"),
 26 | 		),
 27 | 	), handleWiqlQueryFormatPrompt)
 28 | 
 29 | 	// Create Work Item
 30 | 	createWorkItemTool := mcp.NewTool("create_work_item",
 31 | 		mcp.WithDescription("Create a new work item in Azure DevOps"),
 32 | 		mcp.WithString("type",
 33 | 			mcp.Required(),
 34 | 			mcp.Description("Type of work item (Epic, Feature, User Story, Task, Bug)"),
 35 | 			mcp.Enum("Epic", "Feature", "User Story", "Task", "Bug"),
 36 | 		),
 37 | 		mcp.WithString("title",
 38 | 			mcp.Required(),
 39 | 			mcp.Description("Title of the work item"),
 40 | 		),
 41 | 		mcp.WithString("description",
 42 | 			mcp.Required(),
 43 | 			mcp.Description("Description of the work item"),
 44 | 		),
 45 | 		mcp.WithString("priority",
 46 | 			mcp.Description("Priority of the work item (1-4)"),
 47 | 			mcp.Enum("1", "2", "3", "4"),
 48 | 		),
 49 | 	)
 50 | 
 51 | 	s.AddTool(createWorkItemTool, handleCreateWorkItem)
 52 | 
 53 | 	// Update Work Item
 54 | 	updateWorkItemTool := mcp.NewTool("update_work_item",
 55 | 		mcp.WithDescription("Update an existing work item in Azure DevOps"),
 56 | 		mcp.WithNumber("id",
 57 | 			mcp.Required(),
 58 | 			mcp.Description("ID of the work item to update"),
 59 | 		),
 60 | 		mcp.WithString("field",
 61 | 			mcp.Required(),
 62 | 			mcp.Description("Field to update (Title, Description, State, Priority)"),
 63 | 			mcp.Enum("Title", "Description", "State", "Priority"),
 64 | 		),
 65 | 		mcp.WithString("value",
 66 | 			mcp.Required(),
 67 | 			mcp.Description("New value for the field"),
 68 | 		),
 69 | 	)
 70 | 
 71 | 	s.AddTool(updateWorkItemTool, handleUpdateWorkItem)
 72 | 
 73 | 	// Query Work Items
 74 | 	queryWorkItemsTool := mcp.NewTool("query_work_items",
 75 | 		mcp.WithDescription("Query work items using WIQL"),
 76 | 		mcp.WithString("query",
 77 | 			mcp.Required(),
 78 | 			mcp.Description("WIQL query string"),
 79 | 		),
 80 | 	)
 81 | 
 82 | 	s.AddTool(queryWorkItemsTool, handleQueryWorkItems)
 83 | 
 84 | 	// Get Work Item Details
 85 | 	getWorkItemTool := mcp.NewTool("get_work_item_details",
 86 | 		mcp.WithDescription("Get detailed information about work items"),
 87 | 		mcp.WithString("ids",
 88 | 			mcp.Required(),
 89 | 			mcp.Description("Comma-separated list of work item IDs"),
 90 | 		),
 91 | 	)
 92 | 	s.AddTool(getWorkItemTool, handleGetWorkItemDetails)
 93 | 
 94 | 	// Manage Work Item Relations
 95 | 	manageRelationsTool := mcp.NewTool("manage_work_item_relations",
 96 | 		mcp.WithDescription("Manage relationships between work items"),
 97 | 		mcp.WithNumber("source_id",
 98 | 			mcp.Required(),
 99 | 			mcp.Description("ID of the source work item"),
100 | 		),
101 | 		mcp.WithNumber("target_id",
102 | 			mcp.Required(),
103 | 			mcp.Description("ID of the target work item"),
104 | 		),
105 | 		mcp.WithString("relation_type",
106 | 			mcp.Required(),
107 | 			mcp.Description("Type of relationship to manage"),
108 | 			mcp.Enum("parent", "child", "related"),
109 | 		),
110 | 		mcp.WithString("operation",
111 | 			mcp.Required(),
112 | 			mcp.Description("Operation to perform"),
113 | 			mcp.Enum("add", "remove"),
114 | 		),
115 | 	)
116 | 	s.AddTool(manageRelationsTool, handleManageWorkItemRelations)
117 | 
118 | 	// Get Related Work Items
119 | 	getRelatedItemsTool := mcp.NewTool("get_related_work_items",
120 | 		mcp.WithDescription("Get related work items"),
121 | 		mcp.WithNumber("id",
122 | 			mcp.Required(),
123 | 			mcp.Description("ID of the work item to get relations for"),
124 | 		),
125 | 		mcp.WithString("relation_type",
126 | 			mcp.Required(),
127 | 			mcp.Description("Type of relationships to get"),
128 | 			mcp.Enum("parent", "children", "related", "all"),
129 | 		),
130 | 	)
131 | 	s.AddTool(getRelatedItemsTool, handleGetRelatedWorkItems)
132 | 
133 | 	// Comment Management Tool (as Discussion)
134 | 	addCommentTool := mcp.NewTool("add_work_item_comment",
135 | 		mcp.WithDescription("Add a comment to a work item as a discussion"),
136 | 		mcp.WithNumber("id",
137 | 			mcp.Required(),
138 | 			mcp.Description("ID of the work item"),
139 | 		),
140 | 		mcp.WithString("text",
141 | 			mcp.Required(),
142 | 			mcp.Description("Comment text"),
143 | 		),
144 | 	)
145 | 	s.AddTool(addCommentTool, handleAddWorkItemComment)
146 | 
147 | 	getCommentsTool := mcp.NewTool("get_work_item_comments",
148 | 		mcp.WithDescription("Get comments for a work item"),
149 | 		mcp.WithNumber("id",
150 | 			mcp.Required(),
151 | 			mcp.Description("ID of the work item"),
152 | 		),
153 | 	)
154 | 	s.AddTool(getCommentsTool, handleGetWorkItemComments)
155 | 
156 | 	// Field Management Tool
157 | 	getFieldsTool := mcp.NewTool("get_work_item_fields",
158 | 		mcp.WithDescription("Get available work item fields and their current values"),
159 | 		mcp.WithNumber("work_item_id",
160 | 			mcp.Required(),
161 | 			mcp.Description("ID of the work item to examine fields from"),
162 | 		),
163 | 		mcp.WithString("field_name",
164 | 			mcp.Description("Optional field name to filter (case-insensitive partial match)"),
165 | 		),
166 | 	)
167 | 	s.AddTool(getFieldsTool, handleGetWorkItemFields)
168 | 
169 | 	// Batch Operations Tools
170 | 	batchCreateTool := mcp.NewTool("batch_create_work_items",
171 | 		mcp.WithDescription("Create multiple work items in a single operation"),
172 | 		mcp.WithString("items",
173 | 			mcp.Required(),
174 | 			mcp.Description("JSON array of work items to create, each containing type, title, and description"),
175 | 		),
176 | 	)
177 | 	s.AddTool(batchCreateTool, handleBatchCreateWorkItems)
178 | 
179 | 	batchUpdateTool := mcp.NewTool("batch_update_work_items",
180 | 		mcp.WithDescription("Update multiple work items in a single operation"),
181 | 		mcp.WithString("updates",
182 | 			mcp.Required(),
183 | 			mcp.Description("JSON array of updates, each containing id, field, and value"),
184 | 		),
185 | 	)
186 | 	s.AddTool(batchUpdateTool, handleBatchUpdateWorkItems)
187 | 
188 | 	// Tag Management Tools
189 | 	manageTags := mcp.NewTool("manage_work_item_tags",
190 | 		mcp.WithDescription("Add or remove tags from a work item"),
191 | 		mcp.WithNumber("id",
192 | 			mcp.Required(),
193 | 			mcp.Description("ID of the work item"),
194 | 		),
195 | 		mcp.WithString("operation",
196 | 			mcp.Required(),
197 | 			mcp.Description("Operation to perform"),
198 | 			mcp.Enum("add", "remove"),
199 | 		),
200 | 		mcp.WithString("tags",
201 | 			mcp.Required(),
202 | 			mcp.Description("Comma-separated list of tags"),
203 | 		),
204 | 	)
205 | 	s.AddTool(manageTags, handleManageWorkItemTags)
206 | 
207 | 	getTagsTool := mcp.NewTool("get_work_item_tags",
208 | 		mcp.WithDescription("Get tags for a work item"),
209 | 		mcp.WithNumber("id",
210 | 			mcp.Required(),
211 | 			mcp.Description("ID of the work item"),
212 | 		),
213 | 	)
214 | 	s.AddTool(getTagsTool, handleGetWorkItemTags)
215 | 
216 | 	// Work Item Template Tools
217 | 	getTemplatesTool := mcp.NewTool("get_work_item_templates",
218 | 		mcp.WithDescription("Get available work item templates"),
219 | 		mcp.WithString("type",
220 | 			mcp.Required(),
221 | 			mcp.Description("Type of work item to get templates for"),
222 | 			mcp.Enum("Epic", "Feature", "User Story", "Task", "Bug"),
223 | 		),
224 | 	)
225 | 	s.AddTool(getTemplatesTool, handleGetWorkItemTemplates)
226 | 
227 | 	createFromTemplateTool := mcp.NewTool("create_from_template",
228 | 		mcp.WithDescription("Create a work item from a template"),
229 | 		mcp.WithString("template_id",
230 | 			mcp.Required(),
231 | 			mcp.Description("ID of the template to use"),
232 | 		),
233 | 		mcp.WithString("field_values",
234 | 			mcp.Required(),
235 | 			mcp.Description("JSON object of field values to override template defaults"),
236 | 		),
237 | 	)
238 | 	s.AddTool(createFromTemplateTool, handleCreateFromTemplate)
239 | 
240 | 	// Attachment Management Tools
241 | 	addAttachmentTool := mcp.NewTool("add_work_item_attachment",
242 | 		mcp.WithDescription("Add an attachment to a work item"),
243 | 		mcp.WithNumber("id",
244 | 			mcp.Required(),
245 | 			mcp.Description("ID of the work item"),
246 | 		),
247 | 		mcp.WithString("file_name",
248 | 			mcp.Required(),
249 | 			mcp.Description("Name of the file to attach"),
250 | 		),
251 | 		mcp.WithString("content",
252 | 			mcp.Required(),
253 | 			mcp.Description("Base64 encoded content of the file"),
254 | 		),
255 | 	)
256 | 	s.AddTool(addAttachmentTool, handleAddWorkItemAttachment)
257 | 
258 | 	getAttachmentsTool := mcp.NewTool("get_work_item_attachments",
259 | 		mcp.WithDescription("Get attachments for a work item"),
260 | 		mcp.WithNumber("id",
261 | 			mcp.Required(),
262 | 			mcp.Description("ID of the work item"),
263 | 		),
264 | 	)
265 | 	s.AddTool(getAttachmentsTool, handleGetWorkItemAttachments)
266 | 
267 | 	removeAttachmentTool := mcp.NewTool("remove_work_item_attachment",
268 | 		mcp.WithDescription("Remove an attachment from a work item"),
269 | 		mcp.WithNumber("id",
270 | 			mcp.Required(),
271 | 			mcp.Description("ID of the work item"),
272 | 		),
273 | 		mcp.WithString("attachment_id",
274 | 			mcp.Required(),
275 | 			mcp.Description("ID of the attachment to remove"),
276 | 		),
277 | 	)
278 | 	s.AddTool(removeAttachmentTool, handleRemoveWorkItemAttachment)
279 | 
280 | 	// Sprint Management Tools
281 | 	getCurrentSprintTool := mcp.NewTool("get_current_sprint",
282 | 		mcp.WithDescription("Get details about the current sprint"),
283 | 		mcp.WithString("team",
284 | 			mcp.Description("Team name (optional, defaults to project's default team)"),
285 | 		),
286 | 	)
287 | 	s.AddTool(getCurrentSprintTool, handleGetCurrentSprint)
288 | 
289 | 	getSprintsTool := mcp.NewTool("get_sprints",
290 | 		mcp.WithDescription("Get list of sprints"),
291 | 		mcp.WithString("team",
292 | 			mcp.Description("Team name (optional, defaults to project's default team)"),
293 | 		),
294 | 		mcp.WithBoolean("include_completed",
295 | 			mcp.Description("Whether to include completed sprints"),
296 | 		),
297 | 	)
298 | 	s.AddTool(getSprintsTool, handleGetSprints)
299 | 
300 | 	// Add a new prompt for work item descriptions
301 | 	s.AddPrompt(mcp.NewPrompt("format_work_item_description",
302 | 		mcp.WithPromptDescription("Format a work item description using proper HTML for Azure DevOps"),
303 | 		mcp.WithArgument("description",
304 | 			mcp.ArgumentDescription("The description text to format"),
305 | 			mcp.RequiredArgument(),
306 | 		),
307 | 	), handleFormatWorkItemDescription)
308 | }
309 | 
310 | func handleUpdateWorkItem(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
311 | 	id := int(request.Params.Arguments["id"].(float64))
312 | 	field := request.Params.Arguments["field"].(string)
313 | 	value := request.Params.Arguments["value"].(string)
314 | 
315 | 	// Instead of using a fixed map, directly use the field name
316 | 	// This allows any valid Azure DevOps field to be used
317 | 	updateArgs := workitemtracking.UpdateWorkItemArgs{
318 | 		Id:      &id,
319 | 		Project: &config.Project,
320 | 		Document: &[]webapi.JsonPatchOperation{
321 | 			{
322 | 				Op:    &webapi.OperationValues.Replace,
323 | 				Path:  stringPtr("/fields/" + field),
324 | 				Value: value,
325 | 			},
326 | 		},
327 | 	}
328 | 
329 | 	workItem, err := workItemClient.UpdateWorkItem(ctx, updateArgs)
330 | 	if err != nil {
331 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to update work item: %v", err)), nil
332 | 	}
333 | 
334 | 	return mcp.NewToolResultText(fmt.Sprintf("Updated work item #%d", *workItem.Id)), nil
335 | }
336 | 
337 | func handleCreateWorkItem(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
338 | 	workItemType := request.Params.Arguments["type"].(string)
339 | 	title := request.Params.Arguments["title"].(string)
340 | 	description := request.Params.Arguments["description"].(string)
341 | 	priority, hasPriority := request.Params.Arguments["priority"].(string)
342 | 
343 | 	// Create the work item
344 | 	createArgs := workitemtracking.CreateWorkItemArgs{
345 | 		Type:    &workItemType,
346 | 		Project: &config.Project,
347 | 		Document: &[]webapi.JsonPatchOperation{
348 | 			{
349 | 				Op:    &webapi.OperationValues.Add,
350 | 				Path:  stringPtr("/fields/System.Title"),
351 | 				Value: title,
352 | 			},
353 | 			{
354 | 				Op:    &webapi.OperationValues.Add,
355 | 				Path:  stringPtr("/fields/System.Description"),
356 | 				Value: description,
357 | 			},
358 | 		},
359 | 	}
360 | 
361 | 	if hasPriority {
362 | 		doc := append(*createArgs.Document, webapi.JsonPatchOperation{
363 | 			Op:    &webapi.OperationValues.Add,
364 | 			Path:  stringPtr("/fields/Microsoft.VSTS.Common.Priority"),
365 | 			Value: priority,
366 | 		})
367 | 		createArgs.Document = &doc
368 | 	}
369 | 
370 | 	workItem, err := workItemClient.CreateWorkItem(ctx, createArgs)
371 | 	if err != nil {
372 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to create work item: %v", err)), nil
373 | 	}
374 | 
375 | 	fields := *workItem.Fields
376 | 	var extractedTitle string
377 | 	if t, ok := fields["System.Title"].(string); ok {
378 | 		extractedTitle = t
379 | 	}
380 | 	return mcp.NewToolResultText(fmt.Sprintf("Created work item #%d: %s", *workItem.Id, extractedTitle)), nil
381 | }
382 | 
383 | func handleQueryWorkItems(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
384 | 	query := request.Params.Arguments["query"].(string)
385 | 
386 | 	// Create WIQL query
387 | 	wiqlArgs := workitemtracking.QueryByWiqlArgs{
388 | 		Wiql: &workitemtracking.Wiql{
389 | 			Query: &query,
390 | 		},
391 | 		// Ensure we pass the project context
392 | 		Project: &config.Project,
393 | 		// If you have a specific team, you can add it here
394 | 		// Team: &teamName,
395 | 	}
396 | 
397 | 	queryResult, err := workItemClient.QueryByWiql(ctx, wiqlArgs)
398 | 	if err != nil {
399 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to query work items: %v", err)), nil
400 | 	}
401 | 
402 | 	// If no work items found, return a message
403 | 	if queryResult.WorkItems == nil || len(*queryResult.WorkItems) == 0 {
404 | 		return mcp.NewToolResultText("No work items found matching the query."), nil
405 | 	}
406 | 
407 | 	// Format results
408 | 	var results []string
409 | 	
410 | 	// If there are many work items, we should limit how many we retrieve details for
411 | 	maxDetailsToFetch := 20
412 | 	if len(*queryResult.WorkItems) > 0 {
413 | 		// Get the first few work item IDs
414 | 		count := len(*queryResult.WorkItems)
415 | 		if count > maxDetailsToFetch {
416 | 			count = maxDetailsToFetch
417 | 		}
418 | 		
419 | 		// Create a list of IDs to fetch
420 | 		var ids []int
421 | 		for i := 0; i < count; i++ {
422 | 			ids = append(ids, *(*queryResult.WorkItems)[i].Id)
423 | 		}
424 | 		
425 | 		// Get the work item details
426 | 		if len(ids) > 0 {
427 | 			// First add a header line with the total count
428 | 			results = append(results, fmt.Sprintf("Found %d work items. Showing details for the first %d:", 
429 | 				len(*queryResult.WorkItems), count))
430 | 			results = append(results, "")
431 | 			
432 | 			// Fetch details for these work items
433 | 			getArgs := workitemtracking.GetWorkItemsArgs{
434 | 				Ids: &ids,
435 | 			}
436 | 			workItems, err := workItemClient.GetWorkItems(ctx, getArgs)
437 | 			if err == nil && workItems != nil && len(*workItems) > 0 {
438 | 				for _, item := range *workItems {
439 | 					id := *item.Id
440 | 					var title, state, workItemType string
441 | 					
442 | 					if item.Fields != nil {
443 | 						if titleVal, ok := (*item.Fields)["System.Title"]; ok {
444 | 							title = fmt.Sprintf("%v", titleVal)
445 | 						}
446 | 						if stateVal, ok := (*item.Fields)["System.State"]; ok {
447 | 							state = fmt.Sprintf("%v", stateVal)
448 | 						}
449 | 						if typeVal, ok := (*item.Fields)["System.WorkItemType"]; ok {
450 | 							workItemType = fmt.Sprintf("%v", typeVal)
451 | 						}
452 | 					}
453 | 					
454 | 					results = append(results, fmt.Sprintf("ID: %d - [%s] %s (%s)", 
455 | 						id, workItemType, title, state))
456 | 				}
457 | 			} else {
458 | 				// Fallback to just listing the IDs if we couldn't get details
459 | 				for _, itemRef := range *queryResult.WorkItems {
460 | 					results = append(results, fmt.Sprintf("ID: %d", *itemRef.Id))
461 | 				}
462 | 			}
463 | 		}
464 | 	}
465 | 
466 | 	return mcp.NewToolResultText(strings.Join(results, "\n")), nil
467 | }
468 | 
469 | func handleWiqlQueryFormatPrompt(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
470 | 	queryType, exists := request.Params.Arguments["query_type"]
471 | 	if !exists {
472 | 		return nil, fmt.Errorf("query_type is required")
473 | 	}
474 | 
475 | 	additionalFields := request.Params.Arguments["additional_fields"]
476 | 
477 | 	baseFields := "[System.Id], [System.Title], [System.WorkItemType], [System.State], [System.AssignedTo]"
478 | 	if additionalFields != "" {
479 | 		baseFields += ", " + additionalFields
480 | 	}
481 | 
482 | 	var template string
483 | 	var explanation string
484 | 
485 | 	switch queryType {
486 | 	case "current_sprint":
487 | 		template = fmt.Sprintf("SELECT %s FROM WorkItems WHERE [System.IterationPath] = @currentIteration('fanapp')", baseFields)
488 | 		explanation = "This query gets all work items in the current sprint. The @currentIteration macro automatically resolves to the current sprint path."
489 | 
490 | 	case "assigned_to_me":
491 | 		template = fmt.Sprintf("SELECT %s FROM WorkItems WHERE [System.AssignedTo] = @me AND [System.State] <> 'Closed'", baseFields)
492 | 		explanation = "This query gets all active work items assigned to the current user. The @me macro automatically resolves to the current user."
493 | 
494 | 	case "active_bugs":
495 | 		template = fmt.Sprintf("SELECT %s FROM WorkItems WHERE [System.WorkItemType] = 'Bug' AND [System.State] <> 'Closed' ORDER BY [Microsoft.VSTS.Common.Priority]", baseFields)
496 | 		explanation = "This query gets all active bugs, ordered by priority."
497 | 
498 | 	case "blocked_items":
499 | 		template = fmt.Sprintf("SELECT %s FROM WorkItems WHERE [System.State] <> 'Closed' AND [Microsoft.VSTS.Common.Blocked] = 'Yes'", baseFields)
500 | 		explanation = "This query gets all work items that are marked as blocked."
501 | 
502 | 	case "recent_activity":
503 | 		template = fmt.Sprintf("SELECT %s FROM WorkItems WHERE [System.ChangedDate] > @today-7 ORDER BY [System.ChangedDate] DESC", baseFields)
504 | 		explanation = "This query gets all work items modified in the last 7 days, ordered by most recent first."
505 | 	}
506 | 
507 | 	return mcp.NewGetPromptResult(
508 | 		"WIQL Query Format Helper",
509 | 		[]mcp.PromptMessage{
510 | 			mcp.NewPromptMessage(
511 | 				"system",
512 | 				mcp.NewTextContent("You are a WIQL query expert. Help format queries for Azure DevOps work items."),
513 | 			),
514 | 			mcp.NewPromptMessage(
515 | 				"assistant",
516 | 				mcp.NewTextContent(fmt.Sprintf("Here's a template for a %s query:\n\n```sql\n%s\n```\n\n%s\n\nCommon WIQL Tips:\n- Use square brackets [] around field names\n- Common macros: @me, @today, @currentIteration\n- Date arithmetic: @today+/-n\n- String comparison is case-insensitive\n- Use 'Contains' for partial matches", queryType, template, explanation)),
517 | 			),
518 | 		},
519 | 	), nil
520 | }
521 | 
522 | func handleFormatWorkItemDescription(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
523 | 	description := request.Params.Arguments["description"]
524 | 	return mcp.NewGetPromptResult(
525 | 		"Azure DevOps Work Item Description Formatter",
526 | 		[]mcp.PromptMessage{
527 | 			mcp.NewPromptMessage(
528 | 				"system",
529 | 				mcp.NewTextContent("You format work item descriptions for Azure DevOps. Use proper HTML formatting with <ul>, <li> for bullet points, <p> for paragraphs, and <br> for line breaks."),
530 | 			),
531 | 			mcp.NewPromptMessage(
532 | 				"assistant",
533 | 				mcp.NewTextContent(fmt.Sprintf("Here's your description formatted with HTML:\n\n<ul>\n%s\n</ul>",
534 | 					strings.Join(strings.Split(description, "-"), "</li>\n<li>"))),
535 | 			),
536 | 		},
537 | 	), nil
538 | }
539 | 
540 | // Handler for getting detailed work item information
541 | func handleGetWorkItemDetails(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
542 | 	idsStr := request.Params.Arguments["ids"].(string)
543 | 	idStrs := strings.Split(idsStr, ",")
544 | 
545 | 	var ids []int
546 | 	for _, idStr := range idStrs {
547 | 		id, err := strconv.Atoi(strings.TrimSpace(idStr))
548 | 		if err != nil {
549 | 			return mcp.NewToolResultError(fmt.Sprintf("Invalid ID format: %s", idStr)), nil
550 | 		}
551 | 		ids = append(ids, id)
552 | 	}
553 | 
554 | 	workItems, err := workItemClient.GetWorkItems(ctx, workitemtracking.GetWorkItemsArgs{
555 | 		Ids:     &ids,
556 | 		Project: &config.Project,
557 | 		Expand:  &workitemtracking.WorkItemExpandValues.All,
558 | 	})
559 | 
560 | 	if err != nil {
561 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to get work items: %v", err)), nil
562 | 	}
563 | 
564 | 	var results []string
565 | 	for _, item := range *workItems {
566 | 		fields := *item.Fields
567 | 		title, _ := fields["System.Title"].(string)
568 | 		description, _ := fields["System.Description"].(string)
569 | 		state, _ := fields["System.State"].(string)
570 | 
571 | 		result := fmt.Sprintf("ID: %d\nTitle: %s\nState: %s\nDescription: %s\n---\n",
572 | 			*item.Id, title, state, description)
573 | 		results = append(results, result)
574 | 	}
575 | 
576 | 	return mcp.NewToolResultText(strings.Join(results, "\n")), nil
577 | }
578 | 
579 | // Handler for managing work item relationships
580 | func handleManageWorkItemRelations(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
581 | 	sourceID := int(request.Params.Arguments["source_id"].(float64))
582 | 	targetID := int(request.Params.Arguments["target_id"].(float64))
583 | 	relationType, ok := request.Params.Arguments["relation_type"].(string)
584 | 	if !ok {
585 | 		return mcp.NewToolResultError("Invalid relation_type"), nil
586 | 	}
587 | 	operation := request.Params.Arguments["operation"].(string)
588 | 
589 | 	// Map relation types to Azure DevOps relation types
590 | 	relationTypeMap := map[string]string{
591 | 		"parent":  "System.LinkTypes.Hierarchy-Reverse",
592 | 		"child":   "System.LinkTypes.Hierarchy-Forward",
593 | 		"related": "System.LinkTypes.Related",
594 | 	}
595 | 
596 | 	azureRelationType := relationTypeMap[relationType]
597 | 
598 | 	var ops []webapi.JsonPatchOperation
599 | 	if operation == "add" {
600 | 		ops = []webapi.JsonPatchOperation{
601 | 			{
602 | 				Op:   &webapi.OperationValues.Add,
603 | 				Path: stringPtr("/relations/-"),
604 | 				Value: map[string]interface{}{
605 | 					"rel": azureRelationType,
606 | 					"url": fmt.Sprintf("%s/_apis/wit/workItems/%d", config.OrganizationURL, targetID),
607 | 					"attributes": map[string]interface{}{
608 | 						"comment": "Added via MCP",
609 | 					},
610 | 				},
611 | 			},
612 | 		}
613 | 	} else {
614 | 		// For remove, we need to first get the work item to find the relation index
615 | 		workItem, err := workItemClient.GetWorkItem(ctx, workitemtracking.GetWorkItemArgs{
616 | 			Id:      &sourceID,
617 | 			Project: &config.Project,
618 | 		})
619 | 		if err != nil {
620 | 			return mcp.NewToolResultError(fmt.Sprintf("Failed to get work item: %v", err)), nil
621 | 		}
622 | 
623 | 		if workItem.Relations == nil {
624 | 			return mcp.NewToolResultError("Work item has no relations"), nil
625 | 		}
626 | 
627 | 		for i, relation := range *workItem.Relations {
628 | 			if *relation.Rel == azureRelationType {
629 | 				targetUrl := fmt.Sprintf("%s/_apis/wit/workItems/%d", config.OrganizationURL, targetID)
630 | 				if *relation.Url == targetUrl {
631 | 					ops = []webapi.JsonPatchOperation{
632 | 						{
633 | 							Op:   &webapi.OperationValues.Remove,
634 | 							Path: stringPtr(fmt.Sprintf("/relations/%d", i)),
635 | 						},
636 | 					}
637 | 					break
638 | 				}
639 | 			}
640 | 		}
641 | 
642 | 		if len(ops) == 0 {
643 | 			return mcp.NewToolResultError("Specified relation not found"), nil
644 | 		}
645 | 	}
646 | 
647 | 	updateArgs := workitemtracking.UpdateWorkItemArgs{
648 | 		Id:       &sourceID,
649 | 		Project:  &config.Project,
650 | 		Document: &ops,
651 | 	}
652 | 
653 | 	_, err := workItemClient.UpdateWorkItem(ctx, updateArgs)
654 | 	if err != nil {
655 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to update work item relations: %v", err)), nil
656 | 	}
657 | 
658 | 	return mcp.NewToolResultText(fmt.Sprintf("Successfully %sd %s relationship", operation, relationType)), nil
659 | }
660 | 
661 | // Handler for getting related work items
662 | func handleGetRelatedWorkItems(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
663 | 	id := int(request.Params.Arguments["id"].(float64))
664 | 	relationType := request.Params.Arguments["relation_type"].(string)
665 | 
666 | 	workItem, err := workItemClient.GetWorkItem(ctx, workitemtracking.GetWorkItemArgs{
667 | 		Id:      &id,
668 | 		Project: &config.Project,
669 | 		Expand:  &workitemtracking.WorkItemExpandValues.Relations,
670 | 	})
671 | 	if err != nil {
672 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to get work item: %v", err)), nil
673 | 	}
674 | 
675 | 	if workItem.Relations == nil {
676 | 		return mcp.NewToolResultText("No related items found"), nil
677 | 	}
678 | 
679 | 	relationTypeMap := map[string]string{
680 | 		"parent":   "System.LinkTypes.Hierarchy-Reverse",
681 | 		"children": "System.LinkTypes.Hierarchy-Forward",
682 | 		"related":  "System.LinkTypes.Related",
683 | 	}
684 | 
685 | 	// Debug information
686 | 	var debugInfo []string
687 | 	debugInfo = append(debugInfo, fmt.Sprintf("Looking for relation type: %s (mapped to: %s)",
688 | 		relationType, relationTypeMap[relationType]))
689 | 
690 | 	var relatedIds []int
691 | 	for _, relation := range *workItem.Relations {
692 | 		debugInfo = append(debugInfo, fmt.Sprintf("Found relation of type: %s", *relation.Rel))
693 | 
694 | 		if relationType == "all" || *relation.Rel == relationTypeMap[relationType] {
695 | 			parts := strings.Split(*relation.Url, "/")
696 | 			if relatedID, err := strconv.Atoi(parts[len(parts)-1]); err == nil {
697 | 				relatedIds = append(relatedIds, relatedID)
698 | 			}
699 | 		}
700 | 	}
701 | 
702 | 	if len(relatedIds) == 0 {
703 | 		return mcp.NewToolResultText(fmt.Sprintf("Debug info:\n%s\n\nNo matching related items found",
704 | 			strings.Join(debugInfo, "\n"))), nil
705 | 	}
706 | 
707 | 	// Get details of related items
708 | 	relatedItems, err := workItemClient.GetWorkItems(ctx, workitemtracking.GetWorkItemsArgs{
709 | 		Ids:     &relatedIds,
710 | 		Project: &config.Project,
711 | 	})
712 | 	if err != nil {
713 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to get related items: %v", err)), nil
714 | 	}
715 | 
716 | 	var results []string
717 | 	for _, item := range *relatedItems {
718 | 		fields := *item.Fields
719 | 		title, _ := fields["System.Title"].(string)
720 | 		result := fmt.Sprintf("ID: %d, Title: %s", *item.Id, title)
721 | 		results = append(results, result)
722 | 	}
723 | 
724 | 	return mcp.NewToolResultText(strings.Join(results, "\n")), nil
725 | }
726 | 
727 | // Handler for adding a comment to a work item
728 | func handleAddWorkItemComment(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
729 | 	id := int(request.Params.Arguments["id"].(float64))
730 | 	text := request.Params.Arguments["text"].(string)
731 | 
732 | 	// Add comment as a discussion by updating the Discussion field
733 | 	updateArgs := workitemtracking.UpdateWorkItemArgs{
734 | 		Id:      &id,
735 | 		Project: &config.Project,
736 | 		Document: &[]webapi.JsonPatchOperation{
737 | 			{
738 | 				Op:    &webapi.OperationValues.Add,
739 | 				Path:  stringPtr("/fields/System.History"),
740 | 				Value: text,
741 | 			},
742 | 		},
743 | 	}
744 | 
745 | 	workItem, err := workItemClient.UpdateWorkItem(ctx, updateArgs)
746 | 	if err != nil {
747 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to add comment: %v", err)), nil
748 | 	}
749 | 
750 | 	return mcp.NewToolResultText(fmt.Sprintf("Added comment to work item #%d", *workItem.Id)), nil
751 | }
752 | 
753 | // Handler for getting work item comments
754 | func handleGetWorkItemComments(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
755 | 	id := int(request.Params.Arguments["id"].(float64))
756 | 
757 | 	comments, err := workItemClient.GetComments(ctx, workitemtracking.GetCommentsArgs{
758 | 		Project:    &config.Project,
759 | 		WorkItemId: &id,
760 | 	})
761 | 
762 | 	if err != nil {
763 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to get comments: %v", err)), nil
764 | 	}
765 | 
766 | 	var results []string
767 | 	for _, comment := range *comments.Comments {
768 | 		results = append(results, fmt.Sprintf("Comment by %s at %s:\n%s\n---",
769 | 			*comment.CreatedBy.DisplayName,
770 | 			comment.CreatedDate.String(),
771 | 			*comment.Text))
772 | 	}
773 | 
774 | 	return mcp.NewToolResultText(strings.Join(results, "\n")), nil
775 | }
776 | 
777 | // Handler for getting work item fields
778 | func handleGetWorkItemFields(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
779 | 	id := int(request.Params.Arguments["work_item_id"].(float64))
780 | 
781 | 	// Get the work item's details
782 | 	workItem, err := workItemClient.GetWorkItem(ctx, workitemtracking.GetWorkItemArgs{
783 | 		Id:      &id,
784 | 		Project: &config.Project,
785 | 	})
786 | 
787 | 	if err != nil {
788 | 		return mcp.NewToolResultError(fmt.Sprintf("Failed to get work item details: %v", err)), nil
789 | 	}
790 | 
791 | 	// Extract and format field information
792 | 	var results []string
793 | 	fieldName, hasFieldFilter := request.Params.Arguments["field_name"].(string)
794 | 
795 | 	for fieldRef, value := range *workItem.Fields {
796 | 		if hasFieldFilter && !strings.Contains(strings.ToLower(fieldRef), strings.ToLower(fieldName)) {
797 | 			continue
798 | 		}
799 | 
800 | 		results = append(results, fmt.Sprintf("Field: %s\nValue: %v\nType: %T\n---",
801 | 			fieldRef,
802 | 			value,
803 | 			value))
804 | 	}
805 | 
806 | 	if len(results) == 0 {
807 | 		if hasFieldFilter {
808 | 			return mcp.NewToolResultText(fmt.Sprintf("No fields found matching: %s", fieldName)), nil
809 | 		}
810 | 		return mcp.NewToolResultText("No fields found"), nil
811 | 	}
812 | 
813 | 	return mcp.NewToolResultText(strings.Join(results, "\n")), nil
814 | }
815 | 
816 | // Handler for batch creating work items
817 | func handleBatchCreateWorkItems(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
818 | 	itemsJSON := request.Params.Arguments["items"].(string)
819 | 	var items []struct {
820 | 		Type        string `json:"type"`
821 | 		Title       string `json:"title"`
822 | 		Description string `json:"description"`
823 | 		Priority    string `json:"priority,omitempty"`
824 | 	}
825 | 
826 | 	if err := json.Unmarshal([]byte(itemsJSON), &items); err != nil {
827 | 		return mcp.NewToolResultError(fmt.Sprintf("Invalid JSON format: %v", err)), nil
828 | 	}
829 | 
830 | 	var results []string
831 | 	for _, item := range items {
832 | 		createArgs := workitemtracking.CreateWorkItemArgs{
833 | 			Type:    &item.Type,
834 | 			Project: &config.Project,
835 | 			Document: &[]webapi.JsonPatchOperation{
836 | 				{
837 | 					Op:    &webapi.OperationValues.Add,
838 | 					Path:  stringPtr("/fields/System.Title"),
839 | 					Value: item.Title,
840 | 				},
841 | 				{
842 | 					Op:    &webapi.OperationValues.Add,
843 | 					Path:  stringPtr("/fields/System.Description"),
844 | 					Value: item.Description,
845 | 				},
846 | 			},
847 | 		}
848 | 
849 | 		if item.Priority != "" {
850 | 			doc := append(*createArgs.Document, webapi.JsonPatchOperation{
851 | 				Op:    &webapi.OperationValues.Add,
852 | 				Path:  stringPtr("/fields/Microsoft.VSTS.Common.Priority"),
853 | 				Value: item.Priority,
854 | 			})
855 | 			createArgs.Document = &doc
856 | 		}
857 | 
858 | 		workItem, err := workItemClient.CreateWorkItem(ctx, createArgs)
859 | 		if err != nil {
860 | 			results = append(results, fmt.Sprintf("Failed to create '%s': %v", item.Title, err))
861 | 			continue
862 | 		}
863 | 		results = append(results, fmt.Sprintf("Created work item #%d: %s", *workItem.Id, item.Title))
864 | 	}
865 | 
866 | 	return mcp.NewToolResultText(strings.Join(results, "\n")), nil
867 | }
868 | 
869 | // Handler for batch updating work items
870 | func handleBatchUpdateWorkItems(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
871 | 	updatesJSON := request.Params.Arguments["updates"].(string)
872 | 	var updates []struct {
873 | 		ID    int    `json:"id"`
874 | 		Field string `json:"field"`
875 | 		Value string `json:"value"`
876 | 	}
877 | 
878 | 	if err := json.Unmarshal([]byte(updatesJSON), &updates); err != nil {
879 | 		return mcp.NewToolResultError(fmt.Sprintf("Invalid JSON format: %v", err)), nil
880 | 	}
881 | 
882 | 	// Map field names to their System.* equivalents
883 | 	fieldMap := map[string]string{
884 | 		"Title":       "System.Title",
885 | 		"Description": "System.Description",
886 | 		"State":       "System.State",
887 | 		"Priority":    "Microsoft.VSTS.Common.Priority",
888 | 	}
889 | 
890 | 	var results []string
891 | 	for _, update := range updates {
892 | 		systemField, ok := fieldMap[update.Field]
893 | 		if !ok {
894 | 			results = append(results, fmt.Sprintf("Invalid field for #%d: %s", update.ID, update.Field))
895 | 			continue
896 | 		}
897 | 
898 | 		updateArgs := workitemtracking.UpdateWorkItemArgs{
899 | 			Id:      &update.ID,
900 | 			Project: &config.Project,
901 | 			Document: &[]webapi.JsonPatchOperation{
902 | 				{
903 | 					Op:    &webapi.OperationValues.Replace,
904 | 					Path:  stringPtr("/fields/" + systemField),
905 | 					Value: update.Value,
906 | 				},
907 | 			},
908 | 		}
909 | 
910 | 		workItem, err := workItemClient.UpdateWorkItem(ctx, updateArgs)
911 | 		if err != nil {
912 | 			results = append(results, fmt.Sprintf("Failed to update #%d: %v", update.ID, err))
913 | 			continue
914 | 		}
915 | 		results = append(results, fmt.Sprintf("Updated work item #%d", *workItem.Id))
916 | 	}
917 | 
918 | 	return mcp.NewToolResultText(strings.Join(results, "\n")), nil
919 | }
920 | 
```