#
tokens: 18312/50000 9/9 files
lines: off (toggle) GitHub
raw markdown copy
# 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:
--------------------------------------------------------------------------------

```
mcp-azuredevops-bridge
mcp-azuredevops-bridge.exe
start.sh
test-wiki-api.ps1
```

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

```markdown
# MCP Azure DevOps Bridge

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.

## 🌉 Azure DevOps Integration

Connect with Azure DevOps for comprehensive project management:

- **Work Items** - Create, update, query, and manage work items
- **Wiki Documentation** - Create, update, and retrieve wiki pages
- **Sprint Planning** - Retrieve current sprint information and list sprints
- **Attachments & Discussions** - Add and retrieve attachments and comments to/from work items

## 🚀 Getting Started

### Prerequisites

- Go 1.23 or later
- Azure DevOps Personal Access Token (PAT)

### Installation

#### Installing Go 1.23 or above

##### Windows
Install Go using one of these package managers:

1. Using **winget**:
   ```
   winget install GoLang.Go
   ```

2. Using **Chocolatey**:
   ```
   choco install golang
   ```

3. Using **Scoop**:
   ```
   scoop install go
   ```

After installation, verify with:
```
go version
```

##### macOS
Install Go using Homebrew:

```
brew install go
```

Verify the installation:
```
go version
```

#### Building the Project

1. Clone and build:

```bash
git clone https://github.com/krishh-amilineni/mcp-azuredevops-bridge.git
cd mcp-azuredevops-bridge
go build
```

2. Configure your environment:

```bash
export AZURE_DEVOPS_ORG="your-org"
export AZDO_PAT="your-pat-token"
export AZURE_DEVOPS_PROJECT="your-project"
```

3. Add to your Windsurf / Cursor configuration:

```json
{
  "mcpServers": {
    "azuredevops-bridge": {
      "command": "/full/path/to/mcp-azuredevops-bridge/mcp-azuredevops-bridge",
      "args": [],
      "env": {
        "AZURE_DEVOPS_ORG": "organization",
        "AZDO_PAT": "personal_access_token",
        "AZURE_DEVOPS_PROJECT": "project"
      }
    }
  }
}
```

## 💡 Example Workflows

### Work Item Management

```txt
"Create a user story for the new authentication feature in Azure DevOps"
```

### Wiki Documentation

```txt
"Create a wiki page documenting the API endpoints for our service"
"List all wiki pages in our project wiki"
"Get the content of the 'Getting Started' page from the wiki"
"Show me all available wikis in my Azure DevOps project"
```

### Sprint Planning

```txt
"Show me the current sprint's work items and their status"
```

### Attachments and Comments

```txt
"Add this screenshot as an attachment to work item #123"
```

## 🔧 Features

### Work Item Management
- Create new work items (user stories, bugs, tasks, etc.)
- Update existing work items
- Query work items by various criteria
- Link work items to each other

### Wiki Management
- Create and update wiki pages
- Search wiki content
- Retrieve page content and subpages
- Automatic wiki discovery - dynamically finds all available wikis for your project
- Smart wiki selection - selects the most appropriate wiki based on the project context
- Get list of available wikis for debugging and exploration

### Sprint Management
- Get current sprint information
- List all sprints
- View sprint statistics

### Attachments and Comments
- Add attachments to work items
- Retrieve attachments from work items
- Add comments to work items
- View comments on work items

## 📋 Advanced Wiki Usage

The DevOps Bridge includes enhanced wiki functionality that can help you access documentation more effectively:

### Available Wiki Tools

- `list_wiki_pages` - Lists all wiki pages, optionally from a specific path
- `get_wiki_page` - Retrieves the content of a specific wiki page
- `manage_wiki_page` - Creates or updates a wiki page
- `search_wiki` - Searches for content across wiki pages
- `get_available_wikis` - Lists all available wikis in your Azure DevOps organization

### Wiki Troubleshooting

If you're having trouble accessing wiki content:

1. Use the `get_available_wikis` tool to see all available wikis and their IDs
2. Check that your PAT token has appropriate permissions for wiki access
3. Verify that the wiki path is correct - wiki paths are case-sensitive
4. Enable verbose logging to see detailed request and response information

## 🔒 Security

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.

## 📝 Credits

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.

## 📝 License

This project is licensed under the MIT License - see the LICENSE file for details.

## 🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request

```

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

```go
package main

import (
	"context"
	"fmt"
	"strings"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/webapi"
	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/workitemtracking"
)

// Handler for managing work item tags
func handleManageWorkItemTags(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	id := int(request.Params.Arguments["id"].(float64))
	operation := request.Params.Arguments["operation"].(string)
	tagsStr := request.Params.Arguments["tags"].(string)
	tags := strings.Split(tagsStr, ",")

	// Get current work item to get existing tags
	workItem, err := workItemClient.GetWorkItem(ctx, workitemtracking.GetWorkItemArgs{
		Id:      &id,
		Project: &config.Project,
	})
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to get work item: %v", err)), nil
	}

	fields := *workItem.Fields
	var currentTags []string
	if tags, ok := fields["System.Tags"].(string); ok && tags != "" {
		currentTags = strings.Split(tags, "; ")
	}

	var newTags []string
	switch operation {
	case "add":
		// Add new tags while avoiding duplicates
		tagMap := make(map[string]bool)
		for _, tag := range currentTags {
			tagMap[strings.TrimSpace(tag)] = true
		}
		for _, tag := range tags {
			tagMap[strings.TrimSpace(tag)] = true
		}
		for tag := range tagMap {
			newTags = append(newTags, tag)
		}
	case "remove":
		// Remove specified tags
		tagMap := make(map[string]bool)
		for _, tag := range tags {
			tagMap[strings.TrimSpace(tag)] = true
		}
		for _, tag := range currentTags {
			if !tagMap[strings.TrimSpace(tag)] {
				newTags = append(newTags, tag)
			}
		}
	}

	// Update work item with new tags
	updateArgs := workitemtracking.UpdateWorkItemArgs{
		Id:      &id,
		Project: &config.Project,
		Document: &[]webapi.JsonPatchOperation{
			{
				Op:    &webapi.OperationValues.Replace,
				Path:  stringPtr("/fields/System.Tags"),
				Value: strings.Join(newTags, "; "),
			},
		},
	}

	_, err = workItemClient.UpdateWorkItem(ctx, updateArgs)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to update tags: %v", err)), nil
	}

	return mcp.NewToolResultText(fmt.Sprintf("Successfully %sd tags for work item #%d", operation, id)), nil
}

// Handler for getting work item tags
func handleGetWorkItemTags(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	id := int(request.Params.Arguments["id"].(float64))

	workItem, err := workItemClient.GetWorkItem(ctx, workitemtracking.GetWorkItemArgs{
		Id:      &id,
		Project: &config.Project,
	})
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to get work item: %v", err)), nil
	}

	fields := *workItem.Fields
	if tags, ok := fields["System.Tags"].(string); ok && tags != "" {
		return mcp.NewToolResultText(fmt.Sprintf("Tags for work item #%d:\n%s", id, tags)), nil
	}

	return mcp.NewToolResultText(fmt.Sprintf("No tags found for work item #%d", id)), nil
}

```

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

```go
package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"strings"

	"github.com/mark3labs/mcp-go/server"
	"github.com/microsoft/azure-devops-go-api/azuredevops/v7"
	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/core"
	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/wiki"
	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/workitemtracking"
)

// AzureDevOpsConfig holds the configuration for Azure DevOps connection
type AzureDevOpsConfig struct {
	OrganizationURL     string
	PersonalAccessToken string
	Project             string
}

// Global clients and config
var (
	connection     *azuredevops.Connection
	workItemClient workitemtracking.Client
	wikiClient     wiki.Client
	coreClient     core.Client
	config         AzureDevOpsConfig
)

func main() {
	// Main function for the MCP server - handles initialization and startup
	// Load configuration from environment variables
	config = AzureDevOpsConfig{
		OrganizationURL:     "https://dev.azure.com/" + os.Getenv("AZURE_DEVOPS_ORG"),
		PersonalAccessToken: os.Getenv("AZDO_PAT"),
		Project:             os.Getenv("AZURE_DEVOPS_PROJECT"),
	}

	// Validate configuration
	if config.OrganizationURL == "" || config.PersonalAccessToken == "" || config.Project == "" {
		log.Fatal("Missing required environment variables: AZURE_DEVOPS_ORG, AZDO_PAT, AZURE_DEVOPS_PROJECT")
	}

	// Initialize Azure DevOps clients
	if err := initializeClients(config); err != nil {
		log.Fatalf("Failed to initialize Azure DevOps clients: %v", err)
	}

	// Create MCP server
	s := server.NewMCPServer(
		"MCP Azure DevOps Bridge",
		"1.0.0",
		server.WithResourceCapabilities(false, false),
		server.WithPromptCapabilities(true),
		server.WithLogging(),
	)

	// Configure custom error handling
	log.SetFlags(log.LstdFlags | log.Lshortfile)
	log.SetOutput(&logWriter{})

	// Add Work Item tools
	addWorkItemTools(s)

	// Add Wiki tools
	addWikiTools(s)

	// Start the server
	if err := server.ServeStdio(s); err != nil {
		log.Fatalf("Server error: %v\n", err)
	}
}

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func stringPtr(s string) *string {
	return &s
}

// Initialize Azure DevOps clients
func initializeClients(config AzureDevOpsConfig) error {
	connection = azuredevops.NewPatConnection(config.OrganizationURL, config.PersonalAccessToken)

	ctx := context.Background()

	var err error

	// Initialize Work Item Tracking client
	workItemClient, err = workitemtracking.NewClient(ctx, connection)
	if err != nil {
		return fmt.Errorf("failed to create work item client: %v", err)
	}

	// Initialize Wiki client
	wikiClient, err = wiki.NewClient(ctx, connection)
	if err != nil {
		return fmt.Errorf("failed to create wiki client: %v", err)
	}

	// Initialize Core client
	coreClient, err = core.NewClient(ctx, connection)
	if err != nil {
		return fmt.Errorf("failed to create core client: %v", err)
	}

	return nil
}

type logWriter struct{}

func (w *logWriter) Write(bytes []byte) (int, error) {
	// Skip logging "Prompts not supported" errors
	if strings.Contains(string(bytes), "Prompts not supported") {
		return len(bytes), nil
	}
	return fmt.Print(string(bytes))
}

```

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

```go
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"

	"github.com/google/uuid"
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/webapi"
	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/workitemtracking"
)

// Handler for getting work item templates
func handleGetWorkItemTemplates(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	workItemType := request.Params.Arguments["type"].(string)

	templates, err := workItemClient.GetTemplates(ctx, workitemtracking.GetTemplatesArgs{
		Project:          &config.Project,
		Team:             nil, // Get templates for entire project
		Workitemtypename: &workItemType,
	})
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to get templates: %v", err)), nil
	}

	var results []string
	for _, template := range *templates {
		results = append(results, fmt.Sprintf("Template ID: %s\nName: %s\nDescription: %s\n---",
			*template.Id,
			*template.Name,
			*template.Description))
	}

	if len(results) == 0 {
		return mcp.NewToolResultText(fmt.Sprintf("No templates found for type: %s", workItemType)), nil
	}

	return mcp.NewToolResultText(strings.Join(results, "\n")), nil
}

// Handler for creating work item from template
func handleCreateFromTemplate(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	templateID := request.Params.Arguments["template_id"].(string)
	fieldValuesJSON := request.Params.Arguments["field_values"].(string)

	var fieldValues map[string]interface{}
	if err := json.Unmarshal([]byte(fieldValuesJSON), &fieldValues); err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Invalid field values JSON: %v", err)), nil
	}

	// Convert template ID to UUID
	templateUUID, err := uuid.Parse(templateID)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Invalid template ID format: %v", err)), nil
	}

	// Get template
	template, err := workItemClient.GetTemplate(ctx, workitemtracking.GetTemplateArgs{
		Project:    &config.Project,
		Team:       nil,
		TemplateId: &templateUUID,
	})
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to get template: %v", err)), nil
	}

	// Create work item from template
	createArgs := workitemtracking.CreateWorkItemArgs{
		Type:    template.WorkItemTypeName,
		Project: &config.Project,
	}

	// Add template fields
	var operations []webapi.JsonPatchOperation
	for field, value := range *template.Fields {
		operations = append(operations, webapi.JsonPatchOperation{
			Op:    &webapi.OperationValues.Add,
			Path:  stringPtr("/fields/" + field),
			Value: value,
		})
	}

	// Override with provided field values
	for field, value := range fieldValues {
		operations = append(operations, webapi.JsonPatchOperation{
			Op:    &webapi.OperationValues.Add,
			Path:  stringPtr("/fields/" + field),
			Value: value,
		})
	}

	createArgs.Document = &operations

	workItem, err := workItemClient.CreateWorkItem(ctx, createArgs)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to create work item from template: %v", err)), nil
	}

	return mcp.NewToolResultText(fmt.Sprintf("Created work item #%d from template", *workItem.Id)), nil
}

```

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

```go
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/mark3labs/mcp-go/mcp"
)

func handleGetCurrentSprint(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	team, _ := request.Params.Arguments["team"].(string)
	if team == "" {
		team = config.Project + " Team" // Default team name
	}

	// Build the URL for the current iteration
	baseURL := fmt.Sprintf("%s/%s/_apis/work/teamsettings/iterations",
		config.OrganizationURL,
		config.Project)

	queryParams := url.Values{}
	queryParams.Add("$timeframe", "current")
	queryParams.Add("api-version", "7.2-preview")

	fullURL := fmt.Sprintf("%s?%s", baseURL, queryParams.Encode())

	// Create request
	req, err := http.NewRequest("GET", fullURL, nil)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to create request: %v", err)), nil
	}

	// Add authentication
	req.SetBasicAuth("", config.PersonalAccessToken)

	// Send request
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to get current sprint: %v", err)), nil
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to get current sprint. Status: %d", resp.StatusCode)), nil
	}

	// Parse response
	var sprintResponse struct {
		Value []struct {
			Name      string    `json:"name"`
			StartDate time.Time `json:"startDate"`
			EndDate   time.Time `json:"finishDate"`
		} `json:"value"`
	}

	if err := json.NewDecoder(resp.Body).Decode(&sprintResponse); err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to parse response: %v", err)), nil
	}

	if len(sprintResponse.Value) == 0 {
		return mcp.NewToolResultText("No active sprint found"), nil
	}

	sprint := sprintResponse.Value[0]
	result := fmt.Sprintf("Current Sprint: %s\nStart Date: %s\nEnd Date: %s",
		sprint.Name,
		sprint.StartDate.Format("2006-01-02"),
		sprint.EndDate.Format("2006-01-02"))

	return mcp.NewToolResultText(result), nil
}

func handleGetSprints(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	team, _ := request.Params.Arguments["team"].(string)
	includeCompleted, _ := request.Params.Arguments["include_completed"].(bool)
	if team == "" {
		team = config.Project + " Team"
	}

	// Build the URL for iterations
	baseURL := fmt.Sprintf("%s/%s/_apis/work/teamsettings/iterations",
		config.OrganizationURL,
		config.Project)

	queryParams := url.Values{}
	if !includeCompleted {
		queryParams.Add("$timeframe", "current,future")
	}
	queryParams.Add("api-version", "7.2-preview")

	fullURL := fmt.Sprintf("%s?%s", baseURL, queryParams.Encode())

	req, err := http.NewRequest("GET", fullURL, nil)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to create request: %v", err)), nil
	}

	req.SetBasicAuth("", config.PersonalAccessToken)

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to get sprints: %v", err)), nil
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to get sprints. Status: %d", resp.StatusCode)), nil
	}

	var sprintResponse struct {
		Value []struct {
			Name      string    `json:"name"`
			StartDate time.Time `json:"startDate"`
			EndDate   time.Time `json:"finishDate"`
		} `json:"value"`
	}

	if err := json.NewDecoder(resp.Body).Decode(&sprintResponse); err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to parse response: %v", err)), nil
	}

	var results []string
	for _, sprint := range sprintResponse.Value {
		results = append(results, fmt.Sprintf("Sprint: %s\nStart: %s\nEnd: %s\n---",
			sprint.Name,
			sprint.StartDate.Format("2006-01-02"),
			sprint.EndDate.Format("2006-01-02")))
	}

	if len(results) == 0 {
		return mcp.NewToolResultText("No sprints found"), nil
	}

	return mcp.NewToolResultText(strings.Join(results, "\n")), nil
}

```

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

```go
package main

import (
	"bytes"
	"context"
	"encoding/base64"
	"fmt"
	"strings"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/webapi"
	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/workitemtracking"
)

// Handler for adding attachment to work item
func handleAddWorkItemAttachment(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	id := int(request.Params.Arguments["id"].(float64))
	fileName := request.Params.Arguments["file_name"].(string)
	content := request.Params.Arguments["content"].(string)

	// Decode base64 content
	fileContent, err := base64.StdEncoding.DecodeString(content)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Invalid base64 content: %v", err)), nil
	}

	// Create upload stream
	stream := bytes.NewReader(fileContent)

	// Upload attachment
	attachment, err := workItemClient.CreateAttachment(ctx, workitemtracking.CreateAttachmentArgs{
		UploadStream: stream,
		FileName:     &fileName,
		Project:      &config.Project,
	})
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to upload attachment: %v", err)), nil
	}

	// Add attachment reference to work item
	updateArgs := workitemtracking.UpdateWorkItemArgs{
		Id:      &id,
		Project: &config.Project,
		Document: &[]webapi.JsonPatchOperation{
			{
				Op:   &webapi.OperationValues.Add,
				Path: stringPtr("/relations/-"),
				Value: map[string]interface{}{
					"rel": "AttachedFile",
					"url": *attachment.Url,
					"attributes": map[string]interface{}{
						"name": fileName,
					},
				},
			},
		},
	}

	_, err = workItemClient.UpdateWorkItem(ctx, updateArgs)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to add attachment to work item: %v", err)), nil
	}

	return mcp.NewToolResultText(fmt.Sprintf("Added attachment '%s' to work item #%d", fileName, id)), nil
}

// Handler for getting work item attachments
func handleGetWorkItemAttachments(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	id := int(request.Params.Arguments["id"].(float64))

	workItem, err := workItemClient.GetWorkItem(ctx, workitemtracking.GetWorkItemArgs{
		Id:      &id,
		Project: &config.Project,
		Expand:  &workitemtracking.WorkItemExpandValues.Relations,
	})
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to get work item: %v", err)), nil
	}

	if workItem.Relations == nil {
		return mcp.NewToolResultText(fmt.Sprintf("No attachments found for work item #%d", id)), nil
	}

	var results []string
	for _, relation := range *workItem.Relations {
		if *relation.Rel == "AttachedFile" {
			name := (*relation.Attributes)["name"].(string)
			results = append(results, fmt.Sprintf("ID: %s\nName: %s\nURL: %s\n---",
				*relation.Url,
				name,
				*relation.Url))
		}
	}

	if len(results) == 0 {
		return mcp.NewToolResultText(fmt.Sprintf("No attachments found for work item #%d", id)), nil
	}

	return mcp.NewToolResultText(strings.Join(results, "\n")), nil
}

// Handler for removing attachment from work item
func handleRemoveWorkItemAttachment(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	id := int(request.Params.Arguments["id"].(float64))
	attachmentID := request.Params.Arguments["attachment_id"].(string)

	workItem, err := workItemClient.GetWorkItem(ctx, workitemtracking.GetWorkItemArgs{
		Id:      &id,
		Project: &config.Project,
		Expand:  &workitemtracking.WorkItemExpandValues.Relations,
	})
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to get work item: %v", err)), nil
	}

	if workItem.Relations == nil {
		return mcp.NewToolResultError("Work item has no attachments"), nil
	}

	// Find the attachment relation index
	var relationIndex int = -1
	for i, relation := range *workItem.Relations {
		if *relation.Rel == "AttachedFile" && strings.Contains(*relation.Url, attachmentID) {
			relationIndex = i
			break
		}
	}

	if relationIndex == -1 {
		return mcp.NewToolResultError("Attachment not found"), nil
	}

	// Remove the attachment relation
	updateArgs := workitemtracking.UpdateWorkItemArgs{
		Id:      &id,
		Project: &config.Project,
		Document: &[]webapi.JsonPatchOperation{
			{
				Op:   &webapi.OperationValues.Remove,
				Path: stringPtr(fmt.Sprintf("/relations/%d", relationIndex)),
			},
		},
	}

	_, err = workItemClient.UpdateWorkItem(ctx, updateArgs)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to remove attachment: %v", err)), nil
	}

	return mcp.NewToolResultText(fmt.Sprintf("Removed attachment from work item #%d", id)), nil
}

```

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

```go
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"strings"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/wiki"
)

func addWikiTools(s *server.MCPServer) {
	// Wiki Page Management
	manageWikiTool := mcp.NewTool("manage_wiki_page",
		mcp.WithDescription("Create or update a wiki page"),
		mcp.WithString("path",
			mcp.Required(),
			mcp.Description("Path of the wiki page"),
		),
		mcp.WithString("content",
			mcp.Required(),
			mcp.Description("Content of the wiki page in markdown format"),
		),
	)
	s.AddTool(manageWikiTool, handleManageWikiPage)

	// Get Wiki Page
	getWikiTool := mcp.NewTool("get_wiki_page",
		mcp.WithDescription("Get content of a wiki page"),
		mcp.WithString("path",
			mcp.Required(),
			mcp.Description("Path of the wiki page to retrieve"),
		),
		mcp.WithBoolean("include_children",
			mcp.Description("Whether to include child pages"),
		),
	)
	s.AddTool(getWikiTool, handleGetWikiPage)

	// List Wiki Pages
	listWikiTool := mcp.NewTool("list_wiki_pages",
		mcp.WithDescription("List wiki pages in a directory"),
		mcp.WithString("path",
			mcp.Description("Path to list pages from (optional)"),
		),
		mcp.WithBoolean("recursive",
			mcp.Description("Whether to list pages recursively"),
		),
	)
	s.AddTool(listWikiTool, handleListWikiPages)

	// Search Wiki
	searchWikiTool := mcp.NewTool("search_wiki",
		mcp.WithDescription("Search wiki pages"),
		mcp.WithString("query",
			mcp.Required(),
			mcp.Description("Search query"),
		),
		mcp.WithString("path",
			mcp.Description("Path to limit search to (optional)"),
		),
	)
	s.AddTool(searchWikiTool, handleSearchWiki)

	// Get Available Wikis
	getWikisTool := mcp.NewTool("get_available_wikis",
		mcp.WithDescription("Get information about available wikis"),
	)
	s.AddTool(getWikisTool, handleGetWikis)
}

func handleManageWikiPage(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	path := request.Params.Arguments["path"].(string)
	content := request.Params.Arguments["content"].(string)
	// Note: Comments are not supported by the Azure DevOps Wiki API
	_, _ = request.Params.Arguments["comment"].(string)

	// Get all available wikis for the project
	wikis, err := getWikisForProject(ctx)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to get wikis: %v", err)), nil
	}

	if len(wikis) == 0 {
		return mcp.NewToolResultError("No wikis found for this project"), nil
	}

	// Use the first wiki by default, or try to match by project name
	wikiId := *wikis[0].Id
	for _, wiki := range wikis {
		if strings.Contains(*wiki.Name, config.Project) {
			wikiId = *wiki.Id
			break
		}
	}

	// Convert wiki ID to the format expected by the API
	wikiIdentifier := fmt.Sprintf("%s", wikiId)

	_, err = wikiClient.CreateOrUpdatePage(ctx, wiki.CreateOrUpdatePageArgs{
		WikiIdentifier: &wikiIdentifier,
		Path:           &path,
		Project:        &config.Project,
		Parameters: &wiki.WikiPageCreateOrUpdateParameters{
			Content: &content,
		},
	})

	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to manage wiki page: %v", err)), nil
	}

	return mcp.NewToolResultText(fmt.Sprintf("Successfully managed wiki page: %s", path)), nil
}

func handleGetWikiPage(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	path := request.Params.Arguments["path"].(string)
	includeChildren, _ := request.Params.Arguments["include_children"].(bool)

	// Ensure path starts with a forward slash
	if !strings.HasPrefix(path, "/") {
		path = "/" + path
	}

	log.Printf("Wiki page path: %s", path)

	recursionLevel := "none"
	if includeChildren {
		recursionLevel = "oneLevel"
	}

	// Get all available wikis for the project
	wikis, err := getWikisForProject(ctx)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to get wikis: %v", err)), nil
	}

	log.Printf("Found %d wikis for project", len(wikis))
	for i, wiki := range wikis {
		log.Printf("Wiki %d: %s (ID: %s)", i+1, *wiki.Name, *wiki.Id)
	}

	if len(wikis) == 0 {
		return mcp.NewToolResultError("No wikis found for this project"), nil
	}

	// Use the first wiki by default
	wikiId := *wikis[0].Id
	
	// Try to find a wiki with a name that matches or contains the project name
	projectName := strings.Replace(config.Project, " ", "", -1)
	projectName = strings.ToLower(projectName)
	
	for _, wiki := range wikis {
		wikiName := strings.ToLower(*wiki.Name)
		if strings.Contains(wikiName, projectName) || strings.Contains(wikiName, "documentation") {
			wikiId = *wiki.Id
			log.Printf("Selected wiki: %s (ID: %s)", *wiki.Name, wikiId)
			break
		}
	}

	// Build the URL with query parameters
	baseURL := fmt.Sprintf("%s/%s/_apis/wiki/wikis/%s/pages",
		config.OrganizationURL,
		url.PathEscape(config.Project),
		wikiId)

	queryParams := url.Values{}
	queryParams.Add("path", path)
	queryParams.Add("recursionLevel", recursionLevel)
	queryParams.Add("includeContent", "true")
	queryParams.Add("api-version", "7.2-preview")

	fullURL := fmt.Sprintf("%s?%s", baseURL, queryParams.Encode())
	log.Printf("Requesting wiki page from URL: %s", fullURL)

	// Create request
	req, err := http.NewRequest("GET", fullURL, nil)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to create request: %v", err)), nil
	}

	// Add authentication
	req.SetBasicAuth("", config.PersonalAccessToken)

	// Send request
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to get wiki page: %v", err)), nil
	}
	defer resp.Body.Close()

	// Read the response body
	responseBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to read response body: %v", err)), nil
	}

	if resp.StatusCode != http.StatusOK {
		// Log more details about the error
		log.Printf("Wiki API Error - Status: %d, Response: %s", resp.StatusCode, string(responseBody))
		return mcp.NewToolResultError(fmt.Sprintf("Failed to get wiki page. Status: %d", resp.StatusCode)), nil
	}

	// Parse response
	var wikiResponse struct {
		Content  string `json:"content"`
		SubPages []struct {
			Path    string `json:"path"`
			Content string `json:"content"`
		} `json:"subPages"`
	}

	log.Printf("Wiki API Response: %s", string(responseBody))
	
	if err := json.Unmarshal(responseBody, &wikiResponse); err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to parse response: %v", err)), nil
	}

	// Format result
	var result strings.Builder
	result.WriteString(fmt.Sprintf("=== %s ===\n\n", path))
	result.WriteString(wikiResponse.Content)

	if includeChildren && len(wikiResponse.SubPages) > 0 {
		result.WriteString("\n\nSub-pages:\n")
		for _, subPage := range wikiResponse.SubPages {
			result.WriteString(fmt.Sprintf("\n=== %s ===\n", subPage.Path))
			result.WriteString(subPage.Content)
			result.WriteString("\n")
		}
	}

	return mcp.NewToolResultText(result.String()), nil
}

func handleListWikiPages(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	path, _ := request.Params.Arguments["path"].(string)
	recursive, _ := request.Params.Arguments["recursive"].(bool)

	recursionLevel := "oneLevel"
	if recursive {
		recursionLevel = "full"
	}

	// Get all available wikis for the project
	wikis, err := getWikisForProject(ctx)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to get wikis: %v", err)), nil
	}

	if len(wikis) == 0 {
		return mcp.NewToolResultError("No wikis found for this project"), nil
	}

	// Use the first wiki by default, or try to match by project name
	wikiId := *wikis[0].Id
	for _, wiki := range wikis {
		if strings.Contains(*wiki.Name, config.Project) {
			wikiId = *wiki.Id
			break
		}
	}

	// Build the URL with query parameters
	baseURL := fmt.Sprintf("%s/%s/_apis/wiki/wikis/%s/pages",
		config.OrganizationURL,
		url.PathEscape(config.Project),
		wikiId)

	queryParams := url.Values{}
	if path != "" {
		queryParams.Add("path", path)
	}
	queryParams.Add("recursionLevel", recursionLevel)
	queryParams.Add("api-version", "7.2-preview")

	fullURL := fmt.Sprintf("%s?%s", baseURL, queryParams.Encode())

	// Create request
	req, err := http.NewRequest("GET", fullURL, nil)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to create request: %v", err)), nil
	}

	// Add authentication
	req.SetBasicAuth("", config.PersonalAccessToken)

	// Send request
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to list wiki pages: %v", err)), nil
	}
	defer resp.Body.Close()

	// Read the response body
	responseBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to read response body: %v", err)), nil
	}

	if resp.StatusCode != http.StatusOK {
		// Log error details
		log.Printf("Wiki API Error - Status: %d, Response: %s", resp.StatusCode, string(responseBody))
		return mcp.NewToolResultError(fmt.Sprintf("Failed to list wiki pages. Status: %d", resp.StatusCode)), nil
	}

	// Parse response
	var listResponse struct {
		Value []struct {
			Path       string `json:"path"`
			RemotePath string `json:"remotePath"`
			IsFolder   bool   `json:"isFolder"`
		} `json:"value"`
	}

	log.Printf("Wiki API Response: %s", string(responseBody))
	
	if err := json.Unmarshal(responseBody, &listResponse); err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to parse response: %v", err)), nil
	}

	// Format result
	var result strings.Builder
	var locationText string
	if path != "" {
		locationText = " in " + path
	}
	result.WriteString(fmt.Sprintf("Wiki pages%s:\n\n", locationText))

	for _, item := range listResponse.Value {
		prefix := "📄 "
		if item.IsFolder {
			prefix = "📁 "
		}
		result.WriteString(fmt.Sprintf("%s%s\n", prefix, item.Path))
	}

	return mcp.NewToolResultText(result.String()), nil
}

func handleSearchWiki(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	query := request.Params.Arguments["query"].(string)
	path, hasPath := request.Params.Arguments["path"].(string)

	// Get all available wikis for the project
	wikis, err := getWikisForProject(ctx)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to get wikis: %v", err)), nil
	}

	if len(wikis) == 0 {
		return mcp.NewToolResultError("No wikis found for this project"), nil
	}

	// Use the first wiki by default, or try to match by project name
	wikiId := *wikis[0].Id
	for _, wiki := range wikis {
		if strings.Contains(*wiki.Name, config.Project) {
			wikiId = *wiki.Id
			break
		}
	}

	// First, get all pages (potentially under the specified path)
	baseURL := fmt.Sprintf("%s/%s/_apis/wiki/wikis/%s/pages",
		config.OrganizationURL,
		url.PathEscape(config.Project),
		wikiId)

	queryParams := url.Values{}
	queryParams.Add("recursionLevel", "full")
	if hasPath {
		queryParams.Add("path", path)
	}
	queryParams.Add("includeContent", "true")
	queryParams.Add("api-version", "7.2-preview")

	fullURL := fmt.Sprintf("%s?%s", baseURL, queryParams.Encode())

	// Create request
	req, err := http.NewRequest("GET", fullURL, nil)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to create request: %v", err)), nil
	}

	// Add authentication
	req.SetBasicAuth("", config.PersonalAccessToken)

	// Send request
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to search wiki: %v", err)), nil
	}
	defer resp.Body.Close()

	// Read the response body
	responseBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to read response body: %v", err)), nil
	}

	if resp.StatusCode != http.StatusOK {
		// Log error details
		log.Printf("Wiki API Error - Status: %d, Response: %s", resp.StatusCode, string(responseBody))
		return mcp.NewToolResultError(fmt.Sprintf("Failed to search wiki. Status: %d", resp.StatusCode)), nil
	}

	// Parse response
	var searchResponse struct {
		Count int `json:"count"`
		Results []struct {
			FileName    string `json:"fileName"`
			Path        string `json:"path"`
			MatchCount  int    `json:"hitCount"`
			Repository  struct {
				ID string `json:"id"`
			} `json:"repository"`
			Hits []struct {
				Content    string `json:"content"`
				LineNumber int    `json:"startLine"`
			} `json:"hits"`
		} `json:"results"`
	}

	log.Printf("Wiki API Search Response: %s", string(responseBody))
	
	if err := json.Unmarshal(responseBody, &searchResponse); err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to parse response: %v", err)), nil
	}

	// Search through the pages
	var results []string
	queryLower := strings.ToLower(query)
	for _, page := range searchResponse.Results {
		if strings.Contains(strings.ToLower(page.FileName), queryLower) {
			// Extract a snippet of context around the match
			contentLower := strings.ToLower(page.FileName)
			index := strings.Index(contentLower, queryLower)
			start := 0
			if index > 100 {
				start = index - 100
			}
			end := len(page.FileName)
			if index+len(query)+100 < len(page.FileName) {
				end = index + len(query) + 100
			}

			snippet := page.FileName[start:end]
			if start > 0 {
				snippet = "..." + snippet
			}
			if end < len(page.FileName) {
				snippet = snippet + "..."
			}

			results = append(results, fmt.Sprintf("Page: %s\nMatch: %s\n---\n", page.Path, snippet))
		}
	}

	if len(results) == 0 {
		return mcp.NewToolResultText(fmt.Sprintf("No matches found for '%s'", query)), nil
	}

	return mcp.NewToolResultText(fmt.Sprintf("Found %d matches:\n\n%s", len(results), strings.Join(results, "\n"))), nil
}

func handleGetWikis(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	wikis, err := getWikisForProject(ctx)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to get wikis: %v", err)), nil
	}

	if len(wikis) == 0 {
		return mcp.NewToolResultError("No wikis found for this project"), nil
	}

	var result strings.Builder
	result.WriteString(fmt.Sprintf("Found %d wikis for project %s:\n\n", len(wikis), config.Project))

	for i, wiki := range wikis {
		result.WriteString(fmt.Sprintf("%d. Wiki Name: %s\n   Wiki ID: %s\n\n",
			i+1, *wiki.Name, *wiki.Id))
	}

	return mcp.NewToolResultText(result.String()), nil
}

func getWikisForProject(ctx context.Context) ([]*wiki.Wiki, error) {
	// Create request
	wikiApiUrl := fmt.Sprintf("%s/%s/_apis/wiki/wikis?api-version=7.2-preview", 
		config.OrganizationURL,
		url.PathEscape(config.Project))
	log.Printf("Getting wikis from URL: %s", wikiApiUrl)
	
	req, err := http.NewRequest("GET", wikiApiUrl, nil)
	if err != nil {
		return nil, err
	}

	// Add authentication
	req.SetBasicAuth("", config.PersonalAccessToken)

	// Send request
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	log.Printf("Wiki API Status Code: %d", resp.StatusCode)
	
	// Read the response body
	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("Failed to read response body: %v", err)
	}
	
	if resp.StatusCode != http.StatusOK {
		log.Printf("Error response: %s", string(bodyBytes))
		return nil, fmt.Errorf("Failed to get wikis. Status: %d", resp.StatusCode)
	}

	// Parse response
	var wikisResponse struct {
		Value []*wiki.Wiki `json:"value"`
	}
	
	log.Printf("Wiki API Response: %s", string(bodyBytes))
	
	// Unmarshal JSON directly from the bytes
	if err := json.Unmarshal(bodyBytes, &wikisResponse); err != nil {
		return nil, fmt.Errorf("Failed to parse wikis response: %v", err)
	}

	log.Printf("Found %d wikis in total", len(wikisResponse.Value))
	
	// For now, return all wikis since we don't have a reliable way to filter
	// If needed, we can add more specific filtering later
	if len(wikisResponse.Value) > 0 {
		log.Printf("First wiki: Name=%s, ID=%s", *wikisResponse.Value[0].Name, *wikisResponse.Value[0].Id)
	}
	
	return wikisResponse.Value, nil
}

```

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

```go
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"strconv"
	"strings"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/webapi"
	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/workitemtracking"
)

func addWorkItemTools(s *server.MCPServer) {
	// Add WIQL Query Format Prompt
	s.AddPrompt(mcp.NewPrompt("wiql_query_format",
		mcp.WithPromptDescription("Helper for formatting WIQL queries for common scenarios"),
		mcp.WithArgument("query_type",
			mcp.ArgumentDescription("Type of query to format (current_sprint, assigned_to_me, etc)"),
			mcp.RequiredArgument(),
		),
		mcp.WithArgument("additional_fields",
			mcp.ArgumentDescription("Additional fields to include in the SELECT clause"),
		),
	), handleWiqlQueryFormatPrompt)

	// Create Work Item
	createWorkItemTool := mcp.NewTool("create_work_item",
		mcp.WithDescription("Create a new work item in Azure DevOps"),
		mcp.WithString("type",
			mcp.Required(),
			mcp.Description("Type of work item (Epic, Feature, User Story, Task, Bug)"),
			mcp.Enum("Epic", "Feature", "User Story", "Task", "Bug"),
		),
		mcp.WithString("title",
			mcp.Required(),
			mcp.Description("Title of the work item"),
		),
		mcp.WithString("description",
			mcp.Required(),
			mcp.Description("Description of the work item"),
		),
		mcp.WithString("priority",
			mcp.Description("Priority of the work item (1-4)"),
			mcp.Enum("1", "2", "3", "4"),
		),
	)

	s.AddTool(createWorkItemTool, handleCreateWorkItem)

	// Update Work Item
	updateWorkItemTool := mcp.NewTool("update_work_item",
		mcp.WithDescription("Update an existing work item in Azure DevOps"),
		mcp.WithNumber("id",
			mcp.Required(),
			mcp.Description("ID of the work item to update"),
		),
		mcp.WithString("field",
			mcp.Required(),
			mcp.Description("Field to update (Title, Description, State, Priority)"),
			mcp.Enum("Title", "Description", "State", "Priority"),
		),
		mcp.WithString("value",
			mcp.Required(),
			mcp.Description("New value for the field"),
		),
	)

	s.AddTool(updateWorkItemTool, handleUpdateWorkItem)

	// Query Work Items
	queryWorkItemsTool := mcp.NewTool("query_work_items",
		mcp.WithDescription("Query work items using WIQL"),
		mcp.WithString("query",
			mcp.Required(),
			mcp.Description("WIQL query string"),
		),
	)

	s.AddTool(queryWorkItemsTool, handleQueryWorkItems)

	// Get Work Item Details
	getWorkItemTool := mcp.NewTool("get_work_item_details",
		mcp.WithDescription("Get detailed information about work items"),
		mcp.WithString("ids",
			mcp.Required(),
			mcp.Description("Comma-separated list of work item IDs"),
		),
	)
	s.AddTool(getWorkItemTool, handleGetWorkItemDetails)

	// Manage Work Item Relations
	manageRelationsTool := mcp.NewTool("manage_work_item_relations",
		mcp.WithDescription("Manage relationships between work items"),
		mcp.WithNumber("source_id",
			mcp.Required(),
			mcp.Description("ID of the source work item"),
		),
		mcp.WithNumber("target_id",
			mcp.Required(),
			mcp.Description("ID of the target work item"),
		),
		mcp.WithString("relation_type",
			mcp.Required(),
			mcp.Description("Type of relationship to manage"),
			mcp.Enum("parent", "child", "related"),
		),
		mcp.WithString("operation",
			mcp.Required(),
			mcp.Description("Operation to perform"),
			mcp.Enum("add", "remove"),
		),
	)
	s.AddTool(manageRelationsTool, handleManageWorkItemRelations)

	// Get Related Work Items
	getRelatedItemsTool := mcp.NewTool("get_related_work_items",
		mcp.WithDescription("Get related work items"),
		mcp.WithNumber("id",
			mcp.Required(),
			mcp.Description("ID of the work item to get relations for"),
		),
		mcp.WithString("relation_type",
			mcp.Required(),
			mcp.Description("Type of relationships to get"),
			mcp.Enum("parent", "children", "related", "all"),
		),
	)
	s.AddTool(getRelatedItemsTool, handleGetRelatedWorkItems)

	// Comment Management Tool (as Discussion)
	addCommentTool := mcp.NewTool("add_work_item_comment",
		mcp.WithDescription("Add a comment to a work item as a discussion"),
		mcp.WithNumber("id",
			mcp.Required(),
			mcp.Description("ID of the work item"),
		),
		mcp.WithString("text",
			mcp.Required(),
			mcp.Description("Comment text"),
		),
	)
	s.AddTool(addCommentTool, handleAddWorkItemComment)

	getCommentsTool := mcp.NewTool("get_work_item_comments",
		mcp.WithDescription("Get comments for a work item"),
		mcp.WithNumber("id",
			mcp.Required(),
			mcp.Description("ID of the work item"),
		),
	)
	s.AddTool(getCommentsTool, handleGetWorkItemComments)

	// Field Management Tool
	getFieldsTool := mcp.NewTool("get_work_item_fields",
		mcp.WithDescription("Get available work item fields and their current values"),
		mcp.WithNumber("work_item_id",
			mcp.Required(),
			mcp.Description("ID of the work item to examine fields from"),
		),
		mcp.WithString("field_name",
			mcp.Description("Optional field name to filter (case-insensitive partial match)"),
		),
	)
	s.AddTool(getFieldsTool, handleGetWorkItemFields)

	// Batch Operations Tools
	batchCreateTool := mcp.NewTool("batch_create_work_items",
		mcp.WithDescription("Create multiple work items in a single operation"),
		mcp.WithString("items",
			mcp.Required(),
			mcp.Description("JSON array of work items to create, each containing type, title, and description"),
		),
	)
	s.AddTool(batchCreateTool, handleBatchCreateWorkItems)

	batchUpdateTool := mcp.NewTool("batch_update_work_items",
		mcp.WithDescription("Update multiple work items in a single operation"),
		mcp.WithString("updates",
			mcp.Required(),
			mcp.Description("JSON array of updates, each containing id, field, and value"),
		),
	)
	s.AddTool(batchUpdateTool, handleBatchUpdateWorkItems)

	// Tag Management Tools
	manageTags := mcp.NewTool("manage_work_item_tags",
		mcp.WithDescription("Add or remove tags from a work item"),
		mcp.WithNumber("id",
			mcp.Required(),
			mcp.Description("ID of the work item"),
		),
		mcp.WithString("operation",
			mcp.Required(),
			mcp.Description("Operation to perform"),
			mcp.Enum("add", "remove"),
		),
		mcp.WithString("tags",
			mcp.Required(),
			mcp.Description("Comma-separated list of tags"),
		),
	)
	s.AddTool(manageTags, handleManageWorkItemTags)

	getTagsTool := mcp.NewTool("get_work_item_tags",
		mcp.WithDescription("Get tags for a work item"),
		mcp.WithNumber("id",
			mcp.Required(),
			mcp.Description("ID of the work item"),
		),
	)
	s.AddTool(getTagsTool, handleGetWorkItemTags)

	// Work Item Template Tools
	getTemplatesTool := mcp.NewTool("get_work_item_templates",
		mcp.WithDescription("Get available work item templates"),
		mcp.WithString("type",
			mcp.Required(),
			mcp.Description("Type of work item to get templates for"),
			mcp.Enum("Epic", "Feature", "User Story", "Task", "Bug"),
		),
	)
	s.AddTool(getTemplatesTool, handleGetWorkItemTemplates)

	createFromTemplateTool := mcp.NewTool("create_from_template",
		mcp.WithDescription("Create a work item from a template"),
		mcp.WithString("template_id",
			mcp.Required(),
			mcp.Description("ID of the template to use"),
		),
		mcp.WithString("field_values",
			mcp.Required(),
			mcp.Description("JSON object of field values to override template defaults"),
		),
	)
	s.AddTool(createFromTemplateTool, handleCreateFromTemplate)

	// Attachment Management Tools
	addAttachmentTool := mcp.NewTool("add_work_item_attachment",
		mcp.WithDescription("Add an attachment to a work item"),
		mcp.WithNumber("id",
			mcp.Required(),
			mcp.Description("ID of the work item"),
		),
		mcp.WithString("file_name",
			mcp.Required(),
			mcp.Description("Name of the file to attach"),
		),
		mcp.WithString("content",
			mcp.Required(),
			mcp.Description("Base64 encoded content of the file"),
		),
	)
	s.AddTool(addAttachmentTool, handleAddWorkItemAttachment)

	getAttachmentsTool := mcp.NewTool("get_work_item_attachments",
		mcp.WithDescription("Get attachments for a work item"),
		mcp.WithNumber("id",
			mcp.Required(),
			mcp.Description("ID of the work item"),
		),
	)
	s.AddTool(getAttachmentsTool, handleGetWorkItemAttachments)

	removeAttachmentTool := mcp.NewTool("remove_work_item_attachment",
		mcp.WithDescription("Remove an attachment from a work item"),
		mcp.WithNumber("id",
			mcp.Required(),
			mcp.Description("ID of the work item"),
		),
		mcp.WithString("attachment_id",
			mcp.Required(),
			mcp.Description("ID of the attachment to remove"),
		),
	)
	s.AddTool(removeAttachmentTool, handleRemoveWorkItemAttachment)

	// Sprint Management Tools
	getCurrentSprintTool := mcp.NewTool("get_current_sprint",
		mcp.WithDescription("Get details about the current sprint"),
		mcp.WithString("team",
			mcp.Description("Team name (optional, defaults to project's default team)"),
		),
	)
	s.AddTool(getCurrentSprintTool, handleGetCurrentSprint)

	getSprintsTool := mcp.NewTool("get_sprints",
		mcp.WithDescription("Get list of sprints"),
		mcp.WithString("team",
			mcp.Description("Team name (optional, defaults to project's default team)"),
		),
		mcp.WithBoolean("include_completed",
			mcp.Description("Whether to include completed sprints"),
		),
	)
	s.AddTool(getSprintsTool, handleGetSprints)

	// Add a new prompt for work item descriptions
	s.AddPrompt(mcp.NewPrompt("format_work_item_description",
		mcp.WithPromptDescription("Format a work item description using proper HTML for Azure DevOps"),
		mcp.WithArgument("description",
			mcp.ArgumentDescription("The description text to format"),
			mcp.RequiredArgument(),
		),
	), handleFormatWorkItemDescription)
}

func handleUpdateWorkItem(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	id := int(request.Params.Arguments["id"].(float64))
	field := request.Params.Arguments["field"].(string)
	value := request.Params.Arguments["value"].(string)

	// Instead of using a fixed map, directly use the field name
	// This allows any valid Azure DevOps field to be used
	updateArgs := workitemtracking.UpdateWorkItemArgs{
		Id:      &id,
		Project: &config.Project,
		Document: &[]webapi.JsonPatchOperation{
			{
				Op:    &webapi.OperationValues.Replace,
				Path:  stringPtr("/fields/" + field),
				Value: value,
			},
		},
	}

	workItem, err := workItemClient.UpdateWorkItem(ctx, updateArgs)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to update work item: %v", err)), nil
	}

	return mcp.NewToolResultText(fmt.Sprintf("Updated work item #%d", *workItem.Id)), nil
}

func handleCreateWorkItem(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	workItemType := request.Params.Arguments["type"].(string)
	title := request.Params.Arguments["title"].(string)
	description := request.Params.Arguments["description"].(string)
	priority, hasPriority := request.Params.Arguments["priority"].(string)

	// Create the work item
	createArgs := workitemtracking.CreateWorkItemArgs{
		Type:    &workItemType,
		Project: &config.Project,
		Document: &[]webapi.JsonPatchOperation{
			{
				Op:    &webapi.OperationValues.Add,
				Path:  stringPtr("/fields/System.Title"),
				Value: title,
			},
			{
				Op:    &webapi.OperationValues.Add,
				Path:  stringPtr("/fields/System.Description"),
				Value: description,
			},
		},
	}

	if hasPriority {
		doc := append(*createArgs.Document, webapi.JsonPatchOperation{
			Op:    &webapi.OperationValues.Add,
			Path:  stringPtr("/fields/Microsoft.VSTS.Common.Priority"),
			Value: priority,
		})
		createArgs.Document = &doc
	}

	workItem, err := workItemClient.CreateWorkItem(ctx, createArgs)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to create work item: %v", err)), nil
	}

	fields := *workItem.Fields
	var extractedTitle string
	if t, ok := fields["System.Title"].(string); ok {
		extractedTitle = t
	}
	return mcp.NewToolResultText(fmt.Sprintf("Created work item #%d: %s", *workItem.Id, extractedTitle)), nil
}

func handleQueryWorkItems(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	query := request.Params.Arguments["query"].(string)

	// Create WIQL query
	wiqlArgs := workitemtracking.QueryByWiqlArgs{
		Wiql: &workitemtracking.Wiql{
			Query: &query,
		},
		// Ensure we pass the project context
		Project: &config.Project,
		// If you have a specific team, you can add it here
		// Team: &teamName,
	}

	queryResult, err := workItemClient.QueryByWiql(ctx, wiqlArgs)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to query work items: %v", err)), nil
	}

	// If no work items found, return a message
	if queryResult.WorkItems == nil || len(*queryResult.WorkItems) == 0 {
		return mcp.NewToolResultText("No work items found matching the query."), nil
	}

	// Format results
	var results []string
	
	// If there are many work items, we should limit how many we retrieve details for
	maxDetailsToFetch := 20
	if len(*queryResult.WorkItems) > 0 {
		// Get the first few work item IDs
		count := len(*queryResult.WorkItems)
		if count > maxDetailsToFetch {
			count = maxDetailsToFetch
		}
		
		// Create a list of IDs to fetch
		var ids []int
		for i := 0; i < count; i++ {
			ids = append(ids, *(*queryResult.WorkItems)[i].Id)
		}
		
		// Get the work item details
		if len(ids) > 0 {
			// First add a header line with the total count
			results = append(results, fmt.Sprintf("Found %d work items. Showing details for the first %d:", 
				len(*queryResult.WorkItems), count))
			results = append(results, "")
			
			// Fetch details for these work items
			getArgs := workitemtracking.GetWorkItemsArgs{
				Ids: &ids,
			}
			workItems, err := workItemClient.GetWorkItems(ctx, getArgs)
			if err == nil && workItems != nil && len(*workItems) > 0 {
				for _, item := range *workItems {
					id := *item.Id
					var title, state, workItemType string
					
					if item.Fields != nil {
						if titleVal, ok := (*item.Fields)["System.Title"]; ok {
							title = fmt.Sprintf("%v", titleVal)
						}
						if stateVal, ok := (*item.Fields)["System.State"]; ok {
							state = fmt.Sprintf("%v", stateVal)
						}
						if typeVal, ok := (*item.Fields)["System.WorkItemType"]; ok {
							workItemType = fmt.Sprintf("%v", typeVal)
						}
					}
					
					results = append(results, fmt.Sprintf("ID: %d - [%s] %s (%s)", 
						id, workItemType, title, state))
				}
			} else {
				// Fallback to just listing the IDs if we couldn't get details
				for _, itemRef := range *queryResult.WorkItems {
					results = append(results, fmt.Sprintf("ID: %d", *itemRef.Id))
				}
			}
		}
	}

	return mcp.NewToolResultText(strings.Join(results, "\n")), nil
}

func handleWiqlQueryFormatPrompt(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
	queryType, exists := request.Params.Arguments["query_type"]
	if !exists {
		return nil, fmt.Errorf("query_type is required")
	}

	additionalFields := request.Params.Arguments["additional_fields"]

	baseFields := "[System.Id], [System.Title], [System.WorkItemType], [System.State], [System.AssignedTo]"
	if additionalFields != "" {
		baseFields += ", " + additionalFields
	}

	var template string
	var explanation string

	switch queryType {
	case "current_sprint":
		template = fmt.Sprintf("SELECT %s FROM WorkItems WHERE [System.IterationPath] = @currentIteration('fanapp')", baseFields)
		explanation = "This query gets all work items in the current sprint. The @currentIteration macro automatically resolves to the current sprint path."

	case "assigned_to_me":
		template = fmt.Sprintf("SELECT %s FROM WorkItems WHERE [System.AssignedTo] = @me AND [System.State] <> 'Closed'", baseFields)
		explanation = "This query gets all active work items assigned to the current user. The @me macro automatically resolves to the current user."

	case "active_bugs":
		template = fmt.Sprintf("SELECT %s FROM WorkItems WHERE [System.WorkItemType] = 'Bug' AND [System.State] <> 'Closed' ORDER BY [Microsoft.VSTS.Common.Priority]", baseFields)
		explanation = "This query gets all active bugs, ordered by priority."

	case "blocked_items":
		template = fmt.Sprintf("SELECT %s FROM WorkItems WHERE [System.State] <> 'Closed' AND [Microsoft.VSTS.Common.Blocked] = 'Yes'", baseFields)
		explanation = "This query gets all work items that are marked as blocked."

	case "recent_activity":
		template = fmt.Sprintf("SELECT %s FROM WorkItems WHERE [System.ChangedDate] > @today-7 ORDER BY [System.ChangedDate] DESC", baseFields)
		explanation = "This query gets all work items modified in the last 7 days, ordered by most recent first."
	}

	return mcp.NewGetPromptResult(
		"WIQL Query Format Helper",
		[]mcp.PromptMessage{
			mcp.NewPromptMessage(
				"system",
				mcp.NewTextContent("You are a WIQL query expert. Help format queries for Azure DevOps work items."),
			),
			mcp.NewPromptMessage(
				"assistant",
				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)),
			),
		},
	), nil
}

func handleFormatWorkItemDescription(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
	description := request.Params.Arguments["description"]
	return mcp.NewGetPromptResult(
		"Azure DevOps Work Item Description Formatter",
		[]mcp.PromptMessage{
			mcp.NewPromptMessage(
				"system",
				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."),
			),
			mcp.NewPromptMessage(
				"assistant",
				mcp.NewTextContent(fmt.Sprintf("Here's your description formatted with HTML:\n\n<ul>\n%s\n</ul>",
					strings.Join(strings.Split(description, "-"), "</li>\n<li>"))),
			),
		},
	), nil
}

// Handler for getting detailed work item information
func handleGetWorkItemDetails(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	idsStr := request.Params.Arguments["ids"].(string)
	idStrs := strings.Split(idsStr, ",")

	var ids []int
	for _, idStr := range idStrs {
		id, err := strconv.Atoi(strings.TrimSpace(idStr))
		if err != nil {
			return mcp.NewToolResultError(fmt.Sprintf("Invalid ID format: %s", idStr)), nil
		}
		ids = append(ids, id)
	}

	workItems, err := workItemClient.GetWorkItems(ctx, workitemtracking.GetWorkItemsArgs{
		Ids:     &ids,
		Project: &config.Project,
		Expand:  &workitemtracking.WorkItemExpandValues.All,
	})

	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to get work items: %v", err)), nil
	}

	var results []string
	for _, item := range *workItems {
		fields := *item.Fields
		title, _ := fields["System.Title"].(string)
		description, _ := fields["System.Description"].(string)
		state, _ := fields["System.State"].(string)

		result := fmt.Sprintf("ID: %d\nTitle: %s\nState: %s\nDescription: %s\n---\n",
			*item.Id, title, state, description)
		results = append(results, result)
	}

	return mcp.NewToolResultText(strings.Join(results, "\n")), nil
}

// Handler for managing work item relationships
func handleManageWorkItemRelations(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	sourceID := int(request.Params.Arguments["source_id"].(float64))
	targetID := int(request.Params.Arguments["target_id"].(float64))
	relationType, ok := request.Params.Arguments["relation_type"].(string)
	if !ok {
		return mcp.NewToolResultError("Invalid relation_type"), nil
	}
	operation := request.Params.Arguments["operation"].(string)

	// Map relation types to Azure DevOps relation types
	relationTypeMap := map[string]string{
		"parent":  "System.LinkTypes.Hierarchy-Reverse",
		"child":   "System.LinkTypes.Hierarchy-Forward",
		"related": "System.LinkTypes.Related",
	}

	azureRelationType := relationTypeMap[relationType]

	var ops []webapi.JsonPatchOperation
	if operation == "add" {
		ops = []webapi.JsonPatchOperation{
			{
				Op:   &webapi.OperationValues.Add,
				Path: stringPtr("/relations/-"),
				Value: map[string]interface{}{
					"rel": azureRelationType,
					"url": fmt.Sprintf("%s/_apis/wit/workItems/%d", config.OrganizationURL, targetID),
					"attributes": map[string]interface{}{
						"comment": "Added via MCP",
					},
				},
			},
		}
	} else {
		// For remove, we need to first get the work item to find the relation index
		workItem, err := workItemClient.GetWorkItem(ctx, workitemtracking.GetWorkItemArgs{
			Id:      &sourceID,
			Project: &config.Project,
		})
		if err != nil {
			return mcp.NewToolResultError(fmt.Sprintf("Failed to get work item: %v", err)), nil
		}

		if workItem.Relations == nil {
			return mcp.NewToolResultError("Work item has no relations"), nil
		}

		for i, relation := range *workItem.Relations {
			if *relation.Rel == azureRelationType {
				targetUrl := fmt.Sprintf("%s/_apis/wit/workItems/%d", config.OrganizationURL, targetID)
				if *relation.Url == targetUrl {
					ops = []webapi.JsonPatchOperation{
						{
							Op:   &webapi.OperationValues.Remove,
							Path: stringPtr(fmt.Sprintf("/relations/%d", i)),
						},
					}
					break
				}
			}
		}

		if len(ops) == 0 {
			return mcp.NewToolResultError("Specified relation not found"), nil
		}
	}

	updateArgs := workitemtracking.UpdateWorkItemArgs{
		Id:       &sourceID,
		Project:  &config.Project,
		Document: &ops,
	}

	_, err := workItemClient.UpdateWorkItem(ctx, updateArgs)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to update work item relations: %v", err)), nil
	}

	return mcp.NewToolResultText(fmt.Sprintf("Successfully %sd %s relationship", operation, relationType)), nil
}

// Handler for getting related work items
func handleGetRelatedWorkItems(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	id := int(request.Params.Arguments["id"].(float64))
	relationType := request.Params.Arguments["relation_type"].(string)

	workItem, err := workItemClient.GetWorkItem(ctx, workitemtracking.GetWorkItemArgs{
		Id:      &id,
		Project: &config.Project,
		Expand:  &workitemtracking.WorkItemExpandValues.Relations,
	})
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to get work item: %v", err)), nil
	}

	if workItem.Relations == nil {
		return mcp.NewToolResultText("No related items found"), nil
	}

	relationTypeMap := map[string]string{
		"parent":   "System.LinkTypes.Hierarchy-Reverse",
		"children": "System.LinkTypes.Hierarchy-Forward",
		"related":  "System.LinkTypes.Related",
	}

	// Debug information
	var debugInfo []string
	debugInfo = append(debugInfo, fmt.Sprintf("Looking for relation type: %s (mapped to: %s)",
		relationType, relationTypeMap[relationType]))

	var relatedIds []int
	for _, relation := range *workItem.Relations {
		debugInfo = append(debugInfo, fmt.Sprintf("Found relation of type: %s", *relation.Rel))

		if relationType == "all" || *relation.Rel == relationTypeMap[relationType] {
			parts := strings.Split(*relation.Url, "/")
			if relatedID, err := strconv.Atoi(parts[len(parts)-1]); err == nil {
				relatedIds = append(relatedIds, relatedID)
			}
		}
	}

	if len(relatedIds) == 0 {
		return mcp.NewToolResultText(fmt.Sprintf("Debug info:\n%s\n\nNo matching related items found",
			strings.Join(debugInfo, "\n"))), nil
	}

	// Get details of related items
	relatedItems, err := workItemClient.GetWorkItems(ctx, workitemtracking.GetWorkItemsArgs{
		Ids:     &relatedIds,
		Project: &config.Project,
	})
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to get related items: %v", err)), nil
	}

	var results []string
	for _, item := range *relatedItems {
		fields := *item.Fields
		title, _ := fields["System.Title"].(string)
		result := fmt.Sprintf("ID: %d, Title: %s", *item.Id, title)
		results = append(results, result)
	}

	return mcp.NewToolResultText(strings.Join(results, "\n")), nil
}

// Handler for adding a comment to a work item
func handleAddWorkItemComment(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	id := int(request.Params.Arguments["id"].(float64))
	text := request.Params.Arguments["text"].(string)

	// Add comment as a discussion by updating the Discussion field
	updateArgs := workitemtracking.UpdateWorkItemArgs{
		Id:      &id,
		Project: &config.Project,
		Document: &[]webapi.JsonPatchOperation{
			{
				Op:    &webapi.OperationValues.Add,
				Path:  stringPtr("/fields/System.History"),
				Value: text,
			},
		},
	}

	workItem, err := workItemClient.UpdateWorkItem(ctx, updateArgs)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to add comment: %v", err)), nil
	}

	return mcp.NewToolResultText(fmt.Sprintf("Added comment to work item #%d", *workItem.Id)), nil
}

// Handler for getting work item comments
func handleGetWorkItemComments(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	id := int(request.Params.Arguments["id"].(float64))

	comments, err := workItemClient.GetComments(ctx, workitemtracking.GetCommentsArgs{
		Project:    &config.Project,
		WorkItemId: &id,
	})

	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to get comments: %v", err)), nil
	}

	var results []string
	for _, comment := range *comments.Comments {
		results = append(results, fmt.Sprintf("Comment by %s at %s:\n%s\n---",
			*comment.CreatedBy.DisplayName,
			comment.CreatedDate.String(),
			*comment.Text))
	}

	return mcp.NewToolResultText(strings.Join(results, "\n")), nil
}

// Handler for getting work item fields
func handleGetWorkItemFields(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	id := int(request.Params.Arguments["work_item_id"].(float64))

	// Get the work item's details
	workItem, err := workItemClient.GetWorkItem(ctx, workitemtracking.GetWorkItemArgs{
		Id:      &id,
		Project: &config.Project,
	})

	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Failed to get work item details: %v", err)), nil
	}

	// Extract and format field information
	var results []string
	fieldName, hasFieldFilter := request.Params.Arguments["field_name"].(string)

	for fieldRef, value := range *workItem.Fields {
		if hasFieldFilter && !strings.Contains(strings.ToLower(fieldRef), strings.ToLower(fieldName)) {
			continue
		}

		results = append(results, fmt.Sprintf("Field: %s\nValue: %v\nType: %T\n---",
			fieldRef,
			value,
			value))
	}

	if len(results) == 0 {
		if hasFieldFilter {
			return mcp.NewToolResultText(fmt.Sprintf("No fields found matching: %s", fieldName)), nil
		}
		return mcp.NewToolResultText("No fields found"), nil
	}

	return mcp.NewToolResultText(strings.Join(results, "\n")), nil
}

// Handler for batch creating work items
func handleBatchCreateWorkItems(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	itemsJSON := request.Params.Arguments["items"].(string)
	var items []struct {
		Type        string `json:"type"`
		Title       string `json:"title"`
		Description string `json:"description"`
		Priority    string `json:"priority,omitempty"`
	}

	if err := json.Unmarshal([]byte(itemsJSON), &items); err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Invalid JSON format: %v", err)), nil
	}

	var results []string
	for _, item := range items {
		createArgs := workitemtracking.CreateWorkItemArgs{
			Type:    &item.Type,
			Project: &config.Project,
			Document: &[]webapi.JsonPatchOperation{
				{
					Op:    &webapi.OperationValues.Add,
					Path:  stringPtr("/fields/System.Title"),
					Value: item.Title,
				},
				{
					Op:    &webapi.OperationValues.Add,
					Path:  stringPtr("/fields/System.Description"),
					Value: item.Description,
				},
			},
		}

		if item.Priority != "" {
			doc := append(*createArgs.Document, webapi.JsonPatchOperation{
				Op:    &webapi.OperationValues.Add,
				Path:  stringPtr("/fields/Microsoft.VSTS.Common.Priority"),
				Value: item.Priority,
			})
			createArgs.Document = &doc
		}

		workItem, err := workItemClient.CreateWorkItem(ctx, createArgs)
		if err != nil {
			results = append(results, fmt.Sprintf("Failed to create '%s': %v", item.Title, err))
			continue
		}
		results = append(results, fmt.Sprintf("Created work item #%d: %s", *workItem.Id, item.Title))
	}

	return mcp.NewToolResultText(strings.Join(results, "\n")), nil
}

// Handler for batch updating work items
func handleBatchUpdateWorkItems(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	updatesJSON := request.Params.Arguments["updates"].(string)
	var updates []struct {
		ID    int    `json:"id"`
		Field string `json:"field"`
		Value string `json:"value"`
	}

	if err := json.Unmarshal([]byte(updatesJSON), &updates); err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("Invalid JSON format: %v", err)), nil
	}

	// Map field names to their System.* equivalents
	fieldMap := map[string]string{
		"Title":       "System.Title",
		"Description": "System.Description",
		"State":       "System.State",
		"Priority":    "Microsoft.VSTS.Common.Priority",
	}

	var results []string
	for _, update := range updates {
		systemField, ok := fieldMap[update.Field]
		if !ok {
			results = append(results, fmt.Sprintf("Invalid field for #%d: %s", update.ID, update.Field))
			continue
		}

		updateArgs := workitemtracking.UpdateWorkItemArgs{
			Id:      &update.ID,
			Project: &config.Project,
			Document: &[]webapi.JsonPatchOperation{
				{
					Op:    &webapi.OperationValues.Replace,
					Path:  stringPtr("/fields/" + systemField),
					Value: update.Value,
				},
			},
		}

		workItem, err := workItemClient.UpdateWorkItem(ctx, updateArgs)
		if err != nil {
			results = append(results, fmt.Sprintf("Failed to update #%d: %v", update.ID, err))
			continue
		}
		results = append(results, fmt.Sprintf("Updated work item #%d", *workItem.Id))
	}

	return mcp.NewToolResultText(strings.Join(results, "\n")), nil
}

```