diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..620715f6 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,16 @@ +module.exports = { + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended' + ], + env: { + node: true, + jest: true + }, + rules: { + '@typescript-eslint/explicit-function-return-type': 'error', + '@typescript-eslint/no-explicit-any': 'error' + } +}; \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..9c365988 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: '18.x' + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Build + run: npm run build + + - name: Test + run: npm test + + - name: Upload coverage + uses: codecov/codecov-action@v3 \ No newline at end of file diff --git a/README.md b/README.md index 499e3e41..a3b95960 100644 --- a/README.md +++ b/README.md @@ -1,176 +1,48 @@ -# Model Context Protocol servers +# Azure MCP Server -This repository is a collection of *reference implementations* for the [Model Context Protocol](https://modelcontextprotocol.io/) (MCP), as well as references -to community built servers and additional resources. +MCP Server for the Azure DevOps API, enabling project management, repository operations, and more. -The servers in this repository showcase the versatility and extensibility of MCP, demonstrating how it can be used to give Large Language Models (LLMs) secure, controlled access to tools and data sources. -Each MCP server is implemented with either the [Typescript MCP SDK](https://github.com/modelcontextprotocol/typescript-sdk) or [Python MCP SDK](https://github.com/modelcontextprotocol/python-sdk). +## About +This MCP server provides integration with Azure DevOps services through a Model Context Protocol interface. It allows language models to interact with Azure DevOps features including: +- Project management operations +- Repository operations +- Work item tracking +- Build and release pipelines +- And more -## 🌟 Reference Servers +## Installation -These servers aim to demonstrate MCP features and the Typescript and Python SDK. +You can install the server using npm: -- **[AWS KB Retrieval](src/aws-kb-retrieval-server)** - Retrieval from AWS Knowledge Base using Bedrock Agent Runtime -- **[Brave Search](src/brave-search)** - Web and local search using Brave's Search API -- **[EverArt](src/everart)** - AI image generation using various models -- **[Everything](src/everything)** - Reference / test server with prompts, resources, and tools -- **[Fetch](src/fetch)** - Web content fetching and conversion for efficient LLM usage -- **[Filesystem](src/filesystem)** - Secure file operations with configurable access controls -- **[Git](src/git)** - Tools to read, search, and manipulate Git repositories -- **[GitHub](src/github)** - Repository management, file operations, and GitHub API integration -- **[GitLab](src/gitlab)** - GitLab API, enabling project management -- **[Google Drive](src/gdrive)** - File access and search capabilities for Google Drive -- **[Google Maps](src/google-maps)** - Location services, directions, and place details -- **[Memory](src/memory)** - Knowledge graph-based persistent memory system -- **[PostgreSQL](src/postgres)** - Read-only database access with schema inspection -- **[Puppeteer](src/puppeteer)** - Browser automation and web scraping -- **[Sentry](src/sentry)** - Retrieving and analyzing issues from Sentry.io -- **[Sequential Thinking](src/sequentialthinking)** - Dynamic and reflective problem-solving through thought sequences -- **[Slack](src/slack)** - Channel management and messaging capabilities -- **[Sqlite](src/sqlite)** - Database interaction and business intelligence capabilities -- **[Time](src/time)** - Time and timezone conversion capabilities - -## 🀝 Third-Party Servers - -### πŸŽ–οΈ Official Integrations - -Official integrations are maintained by companies building production ready MCP servers for their platforms. - -- Axiom Logo **[Axiom](https://github.com/axiomhq/mcp-server-axiom)** - Query and analyze your Axiom logs, traces, and all other event data in natural language -- Browserbase Logo **[Browserbase](https://github.com/browserbase/mcp-server-browserbase)** - Automate browser interactions in the cloud (e.g. web navigation, data extraction, form filling, and more) -- **[Cloudflare](https://github.com/cloudflare/mcp-server-cloudflare)** - Deploy, configure & interrogate your resources on the Cloudflare developer platform (e.g. Workers/KV/R2/D1) -- **[Raygun](https://github.com/MindscapeHQ/mcp-server-raygun)** - Interact with your crash reporting and real using monitoring data on your Raygun account -- **[Obsidian Markdown Notes](https://github.com/calclavia/mcp-obsidian)** - Read and search through your Obsidian vault or any directory containing Markdown notes -- E2B Logo **[E2B](https://github.com/e2b-dev/mcp-server)** - Run code in secure sandboxes hosted by [E2B](https://e2b.dev) -- Exa Logo **[Exa](https://github.com/exa-labs/exa-mcp-server)** - Search Engine made for AIs by [Exa](https://exa.ai) -- **[JetBrains](https://github.com/JetBrains/mcp-jetbrains)** – Work on your code with JetBrains IDEs -- **[Needle](https://github.com/JANHMS/needle-mcp)** - Production-ready RAG out of the box to search and retrieve data from your own documents. -- **[Neon](https://github.com/neondatabase/mcp-server-neon)** - Interact with the Neon serverless Postgres platform -- Neo4j Logo **[Neo4j](https://github.com/neo4j-contrib/mcp-neo4j/)** - Neo4j graph database server (schema + read/write-cypher) and separate graph database backed memory -- Tinybird Logo **[Tinybird](https://github.com/tinybirdco/mcp-tinybird)** - Interact with Tinybird serverless ClickHouse platform -- [Search1API](https://github.com/fatwang2/search1api-mcp) - One API for Search, Crawling, and Sitemaps -- **[Qdrant](https://github.com/qdrant/mcp-server-qdrant/)** - Implement semantic memory layer on top of the Qdrant vector search engine -- **[Metoro](https://github.com/metoro-io/metoro-mcp-server)** - Query and interact with kubernetes environments monitored by Metoro - - -### 🌎 Community Servers - -A growing set of community-developed and maintained servers demonstrates various applications of MCP across different domains. - -> **Note:** Community servers are **untested** and should be used at **your own risk**. They are not affiliated with or endorsed by Anthropic. - -- **[MCP Installer](https://github.com/anaisbetts/mcp-installer)** - This server is a server that installs other MCP servers for you. -- **[NS Travel Information](https://github.com/r-huijts/ns-mcp-server)** - Access Dutch Railways (NS) real-time train travel information and disruptions through the official NS API. -- **[Spotify](https://github.com/varunneal/spotify-mcp)** - This MCP allows an LLM to play and use Spotify. -- **[Inoyu](https://github.com/sergehuber/inoyu-mcp-unomi-server)** - Interact with an Apache Unomi CDP customer data platform to retrieve and update customer profiles -- **[Vega-Lite](https://github.com/isaacwasserman/mcp-vegalite-server)** - Generate visualizations from fetched data using the VegaLite format and renderer. -- **[Snowflake](https://github.com/isaacwasserman/mcp-snowflake-server)** - This MCP server enables LLMs to interact with Snowflake databases, allowing for secure and controlled data operations. -- **[MySQL](https://github.com/designcomputer/mysql_mcp_server)** (by DesignComputer) - MySQL database integration in Python with configurable access controls and schema inspection -- **[MySQL](https://github.com/benborla/mcp-server-mysql)** (by benborla) - MySQL database integration in NodeJS with configurable access controls and schema inspection -- **[MSSQL](https://github.com/aekanun2020/mcp-server/)** - MSSQL database integration with configurable access controls and schema inspection -- **[BigQuery](https://github.com/LucasHild/mcp-server-bigquery)** (by LucasHild) - This server enables LLMs to inspect database schemas and execute queries on BigQuery. -- **[BigQuery](https://github.com/ergut/mcp-bigquery-server)** (by ergut) - Server implementation for Google BigQuery integration that enables direct BigQuery database access and querying capabilities -- **[Todoist](https://github.com/abhiz123/todoist-mcp-server)** - Interact with Todoist to manage your tasks. -- **[Tavily search](https://github.com/RamXX/mcp-tavily)** - An MCP server for Tavily's search & news API, with explicit site inclusions/exclusions -- **[Linear](https://github.com/jerhadf/linear-mcp-server)** - Allows LLM to interact with Linear's API for project management, including searching, creating, and updating issues. -- **[Playwright](https://github.com/executeautomation/mcp-playwright)** - This MCP Server will help you run browser automation and webscraping using Playwright -- **[AWS](https://github.com/rishikavikondala/mcp-server-aws)** - Perform operations on your AWS resources using an LLM -- **[LlamaCloud](https://github.com/run-llama/mcp-server-llamacloud)** (by marcusschiesser) - Integrate the data stored in a managed index on [LlamaCloud](https://cloud.llamaindex.ai/) -- **[Any Chat Completions](https://github.com/pyroprompts/any-chat-completions-mcp)** - Interact with any OpenAI SDK Compatible Chat Completions API like OpenAI, Perplexity, Groq, xAI and many more. -- **[Windows CLI](https://github.com/SimonB97/win-cli-mcp-server)** - MCP server for secure command-line interactions on Windows systems, enabling controlled access to PowerShell, CMD, and Git Bash shells. -- **[OpenRPC](https://github.com/shanejonas/openrpc-mpc-server)** - Interact with and discover JSON-RPC APIs via [OpenRPC](https://open-rpc.org). -- **[FireCrawl](https://github.com/vrknetha/mcp-server-firecrawl)** - Advanced web scraping with JavaScript rendering, PDF support, and smart rate limiting -- **[AlphaVantage](https://github.com/calvernaz/alphavantage)** - MCP server for stock market data API [AlphaVantage](https://www.alphavantage.co) -- **[Docker](https://github.com/ckreiling/mcp-server-docker)** - Integrate with Docker to manage containers, images, volumes, and networks. -- **[Kubernetes](https://github.com/Flux159/mcp-server-kubernetes)** - Connect to Kubernetes cluster and manage pods, deployments, and services. -- **[OpenAPI](https://github.com/snaggle-ai/openapi-mcp-server)** - Interact with [OpenAPI](https://www.openapis.org/) APIs. -- **[Pandoc](https://github.com/vivekVells/mcp-pandoc)** - MCP server for seamless document format conversion using Pandoc, supporting Markdown, HTML, and plain text, with other formats like PDF, csv and docx in development. -- **[Pinecone](https://github.com/sirmews/mcp-pinecone)** - MCP server for searching and uploading records to Pinecone. Allows for simple RAG features, leveraging Pinecone's Inference API. -- **[HuggingFace Spaces](https://github.com/evalstate/mcp-hfspace)** - Server for using HuggingFace Spaces, supporting Open Source Image, Audio, Text Models and more. Claude Desktop mode for easy integration. -- **[ChatSum](https://github.com/chatmcp/mcp-server-chatsum)** - Query and Summarize chat messages with LLM. by [mcpso](https://mcp.so) -- **[Rememberizer AI](https://github.com/skydeckai/mcp-server-rememberizer)** - An MCP server designed for interacting with the Rememberizer data source, facilitating enhanced knowledge retrieval. -- **[FlightRadar24](https://github.com/sunsetcoder/flightradar24-mcp-server)** - A Claude Desktop MCP server that helps you track flights in real-time using Flightradar24 data. -- **[X (Twitter)](https://github.com/vidhupv/x-mcp)** (by vidhupv) - Create, manage and publish X/Twitter posts directly through Claude chat. -- **[X (Twitter)](https://github.com/EnesCinr/twitter-mcp)** (by EnesCinr) - Interact with twitter API. Post tweets and search for tweets by query. -- **[RAG Web Browser](https://github.com/apify/mcp-server-rag-web-browser)** An MCP server for Apify's RAG Web Browser Actor to perform web searches, scrape URLs, and return content in Markdown. -- **[XMind](https://github.com/apeyroux/mcp-xmind)** - Read and search through your XMind directory containing XMind files. -- **[oatpp-mcp](https://github.com/oatpp/oatpp-mcp)** - C++ MCP integration for Oat++. Use [Oat++](https://oatpp.io) to build MCP servers. -- **[Contentful-mcp](https://github.com/ivo-toby/contentful-mcp)** - Read, update, delete, publish content in your [Contentful](https://contentful.com) space(s) from this MCP Server. -- **[Home Assistant](https://github.com/tevonsb/homeassistant-mcp)** - Interact with [Home Assistant](https://www.home-assistant.io/) including viewing and controlling lights, switches, sensors, and all other Home Assistant entities. -- **[cognee-mcp](https://github.com/topoteretes/cognee-mcp-server)** - GraphRAG memory server with customizable ingestion, data processing and search -- **[Airtable](https://github.com/domdomegg/airtable-mcp-server)** - Read and write access to [Airtable](https://airtable.com/) databases, with schema inspection. -- **[mcp-k8s-go](https://github.com/strowk/mcp-k8s-go)** - Golang-based Kubernetes server for MCP to browse pods and their logs, events, namespaces and more. Built to be extensible. -- **[Notion](https://github.com/v-3/notion-server)** (by v-3) - Notion MCP integration. Search, Read, Update, and Create pages through Claude chat. -- **[Notion](https://github.com/suekou/mcp-notion-server)** (by suekou) - Interact with Notion API. -- **[TMDB](https://github.com/Laksh-star/mcp-server-tmdb)** - This MCP server integrates with The Movie Database (TMDB) API to provide movie information, search capabilities, and recommendations. -- **[MongoDB](https://github.com/kiliczsh/mcp-mongo-server)** - A Model Context Protocol Server for MongoDB. -- **[Airtable](https://github.com/felores/airtable-mcp)** - Airtable Model Context Protocol Server. -- **[Atlassian](https://github.com/sooperset/mcp-atlassian)** - Interact with Atlassian Cloud products (Confluence and Jira) including searching/reading Confluence spaces/pages, accessing Jira issues, and project metadata. -- **[Google Tasks](https://github.com/zcaceres/gtasks-mcp)** - Google Tasks API Model Context Protocol Server. -- **[Fetch](https://github.com/zcaceres/fetch-mcp)** - A server that flexibly fetches HTML, JSON, Markdown, or plaintext - -## πŸ“š Resources - -Additional resources on MCP. - -- **[Awesome MCP Servers by punkpeye](https://github.com/punkpeye/awesome-mcp-servers)** (**[website](https://glama.ai/mcp/servers)**) - A curated list of MCP servers by **[Frank Fiegel](https://github.com/punkpeye)** -- **[Awesome MCP Servers by wong2](https://github.com/wong2/awesome-mcp-servers)** (**[website](https://mcpservers.org)**) - A curated list of MCP servers by **[wong2](https://github.com/wong2)** -- **[Awesome MCP Servers by appcypher](https://github.com/appcypher/awesome-mcp-servers)** - A curated list of MCP servers by **[Stephen Akinyemi](https://github.com/appcypher)** -- **[Open-Sourced MCP Servers Directory](https://github.com/chatmcp/mcp-directory)** - A curated list of MCP servers by **[mcpso](https://mcp.so)** -- **[Discord Server](https://glama.ai/mcp/discord)** – A community discord server dedicated to MCP by **[Frank Fiegel](https://github.com/punkpeye)** -- **[Smithery](https://smithery.ai/)** - A registry of MCP servers to find the right tools for your LLM agents by **[Henry Mao](https://github.com/calclavia)** -- **[mcp-get](https://mcp-get.com)** - Command line tool for installing and managing MCP servers by **[Michael Latman](https://github.com/michaellatman)** -- **[mcp-cli](https://github.com/wong2/mcp-cli)** - A CLI inspector for the Model Context Protocol by **[wong2](https://github.com/wong2)** -- **[r/mcp](https://www.reddit.com/r/mcp)** – A Reddit community dedicated to MCP by **[Frank Fiegel](https://github.com/punkpeye)** -- **[MCP X Community](https://x.com/i/communities/1861891349609603310)** – A X community for MCP by **[Xiaoyi](https://x.com/chxy)** -- **[mcp-manager](https://github.com/zueai/mcp-manager)** - Simple Web UI to install and manage MCP servers for Claude Desktop by **[Zue](https://github.com/zueai)** -- **[MCPHub](https://github.com/Jeamee/MCPHub-Desktop)** – An Open Source MacOS & Windows GUI Desktop app for discovering, installing and managing MCP servers by **[Jeamee](https://github.com/jeamee)** - -## πŸš€ Getting Started - -### Using MCP Servers in this Repository -Typescript-based servers in this repository can be used directly with `npx`. - -For example, this will start the [Memory](src/memory) server: -```sh -npx -y @modelcontextprotocol/server-memory +```bash +npm install azure-mcp-server ``` -Python-based servers in this repository can be used directly with [`uvx`](https://docs.astral.sh/uv/concepts/tools/) or [`pip`](https://pypi.org/project/pip/). `uvx` is recommended for ease of use and setup. - -For example, this will start the [Git](src/git) server: -```sh -# With uvx -uvx mcp-server-git - -# With pip -pip install mcp-server-git -python -m mcp_server_git -``` +## Using with MCP Client -Follow [these](https://docs.astral.sh/uv/getting-started/installation/) instructions to install `uv` / `uvx` and [these](https://pip.pypa.io/en/stable/installation/) to install `pip`. - -### Using an MCP Client -However, running a server on its own isn't very useful, and should instead be configured into an MCP client. For example, here's the Claude Desktop configuration to use the above server: +Add this to your MCP client configuration (e.g. Claude Desktop): ```json { "mcpServers": { - "memory": { + "azure": { "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-memory"] + "args": ["-y", "azure-mcp-server"], + "env": { + "AZURE_PERSONAL_ACCESS_TOKEN": "" + } } } } ``` -Additional examples of using the Claude Desktop as an MCP client might look like: +For reference, here are additional examples of configuring other MCP servers: ```json { "mcpServers": { "filesystem": { - "command": "npx", + "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/files"] }, "git": { @@ -192,30 +64,7 @@ Additional examples of using the Claude Desktop as an MCP client might look like } ``` -## πŸ› οΈ Creating Your Own Server - -Interested in creating your own MCP server? Visit the official documentation at [modelcontextprotocol.io](https://modelcontextprotocol.io/introduction) for comprehensive guides, best practices, and technical details on implementing MCP servers. - -## 🀝 Contributing - -See [CONTRIBUTING.md](CONTRIBUTING.md) for information about contributing to this repository. - -## πŸ”’ Security - -See [SECURITY.md](SECURITY.md) for reporting security vulnerabilities. - -## πŸ“œ License - -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. - -## πŸ’¬ Community - -- [GitHub Discussions](https://github.com/orgs/modelcontextprotocol/discussions) - -## ⭐ Support - -If you find MCP servers useful, please consider starring the repository and contributing new servers or improvements! - ---- - -Managed by Anthropic, but built together with the community. The Model Context Protocol is open source and we encourage everyone to contribute their own servers and improvements! +## Author +- **Zubeid Hendricks** + - GitHub: @ZubeidHendricks + - Contact: zubeid.hendricks@gmail.com \ No newline at end of file diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..6924c992 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,16 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + coverageThreshold: { + global: { + branches: 80, + functions: 80, + lines: 80, + statements: 80 + } + }, + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/types/**/*.ts' + ] +}; \ No newline at end of file diff --git a/package.json b/package.json index 9d5e5ee2..62e6ea7e 100644 --- a/package.json +++ b/package.json @@ -1,33 +1,42 @@ { - "name": "@modelcontextprotocol/servers", - "private": true, - "version": "0.6.2", - "description": "Model Context Protocol servers", - "license": "MIT", - "author": "Anthropic, PBC (https://anthropic.com)", - "homepage": "https://modelcontextprotocol.io", - "bugs": "https://github.com/modelcontextprotocol/servers/issues", - "type": "module", - "workspaces": [ - "src/*" - ], - "files": [], + "name": "@modelcontextprotocol/server-azure-devops", + "version": "0.1.0", + "description": "Azure DevOps MCP Server Implementation", + "main": "dist/index.js", + "types": "dist/index.d.ts", "scripts": { - "build": "npm run build --workspaces", - "watch": "npm run watch --workspaces", - "publish-all": "npm publish --workspaces --access public", - "link-all": "npm link --workspaces" + "build": "tsc", + "prepare": "npm run build", + "test": "jest --coverage", + "lint": "eslint . --ext .ts", + "format": "prettier --write \"src/**/*.ts\"" }, "dependencies": { - "@modelcontextprotocol/server-everything": "*", - "@modelcontextprotocol/server-gdrive": "*", - "@modelcontextprotocol/server-postgres": "*", - "@modelcontextprotocol/server-puppeteer": "*", - "@modelcontextprotocol/server-slack": "*", - "@modelcontextprotocol/server-brave-search": "*", - "@modelcontextprotocol/server-memory": "*", - "@modelcontextprotocol/server-filesystem": "*", - "@modelcontextprotocol/server-everart": "*", - "@modelcontextprotocol/server-sequential-thinking": "*" + "@modelcontextprotocol/typescript-sdk": "^0.1.0", + "azure-devops-node-api": "^12.0.0" + }, + "devDependencies": { + "@types/jest": "^29.5.0", + "@types/node": "^18.0.0", + "@typescript-eslint/eslint-plugin": "^5.0.0", + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^8.0.0", + "jest": "^29.5.0", + "prettier": "^2.0.0", + "ts-jest": "^29.5.0", + "typescript": "^5.0.0" + }, + "files": [ + "dist", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/modelcontextprotocol/servers.git" } -} +} \ No newline at end of file diff --git a/src/azure-devops/.eslintrc.js b/src/azure-devops/.eslintrc.js new file mode 100644 index 00000000..45fe9487 --- /dev/null +++ b/src/azure-devops/.eslintrc.js @@ -0,0 +1,19 @@ +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended' + ], + rules: { + '@typescript-eslint/explicit-function-return-type': 'error', + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + }, + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module' + } +} \ No newline at end of file diff --git a/src/azure-devops/.github/workflows/ci.yml b/src/azure-devops/.github/workflows/ci.yml new file mode 100644 index 00000000..54f1912d --- /dev/null +++ b/src/azure-devops/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: CI + +on: + push: + paths: + - 'src/azure-devops/**' + pull_request: + paths: + - 'src/azure-devops/**' + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [16.x, 18.x] + + steps: + - uses: actions/checkout@v3 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + working-directory: ./src/azure-devops + run: npm ci + + - name: Lint + working-directory: ./src/azure-devops + run: npm run lint + + - name: Build + working-directory: ./src/azure-devops + run: npm run build + + - name: Test + working-directory: ./src/azure-devops + run: npm test + + - name: Upload coverage + uses: codecov/codecov-action@v3 + with: + directory: ./src/azure-devops/coverage \ No newline at end of file diff --git a/src/azure-devops/CHANGELOG.md b/src/azure-devops/CHANGELOG.md new file mode 100644 index 00000000..739ae66e --- /dev/null +++ b/src/azure-devops/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +All notable changes to the Azure DevOps MCP Server will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1.0] - 2024-12-27 + +### Added +- Initial release +- Work Item Management API + - Create work items + - Get work item by ID + - Update work items +- Project Management API + - List projects + - Get project details + - Create/delete projects +- Repository Management API + - List repositories + - Create/delete repositories + - Get repository branches + +[0.1.0]: https://github.com/modelcontextprotocol/servers/releases/tag/azure-devops-v0.1.0 \ No newline at end of file diff --git a/src/azure-devops/README.md b/src/azure-devops/README.md new file mode 100644 index 00000000..bbbbfeeb --- /dev/null +++ b/src/azure-devops/README.md @@ -0,0 +1,181 @@ +# Azure DevOps MCP Server + +A Model Context Protocol (MCP) server implementation for Azure DevOps, enabling AI language models to interact with Azure DevOps services through a standardized interface. + +## Features + +### Work Item Management +- Create, read, and update work items +- Track bugs, tasks, and user stories +- Manage work item states + +### Project Management +- List and view projects +- Create new projects +- Delete existing projects + +### Repository Management +- List repositories in projects +- Create and delete repositories +- Manage repository branches + +## Installation + +```bash +npm install @modelcontextprotocol/server-azure-devops +``` + +## Configuration + +Set the following environment variables: +- `AZURE_DEVOPS_ORG_URL`: Your Azure DevOps organization URL (e.g., https://dev.azure.com/yourorg) +- `AZURE_PERSONAL_ACCESS_TOKEN`: Your Azure DevOps Personal Access Token + +## Using with MCP Client + +Add this to your MCP client configuration (e.g. Claude Desktop): + +```json +{ + "mcpServers": { + "azure": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-azure-devops"], + "env": { + "AZURE_DEVOPS_ORG_URL": "https://dev.azure.com/yourorg", + "AZURE_PERSONAL_ACCESS_TOKEN": "" + } + } + } +} +``` + +## API Documentation + +### Work Items + +#### Get Work Item +```typescript +getWorkItem({ id: number }): Promise +``` +Retrieves a work item by its ID. + +#### Create Work Item +```typescript +createWorkItem({ + project: string, + type: string, + title: string, + description?: string +}): Promise +``` +Creates a new work item in the specified project. + +#### Update Work Item +```typescript +updateWorkItem({ + id: number, + title?: string, + state?: string, + description?: string +}): Promise +``` +Updates an existing work item. + +### Projects + +#### List Projects +```typescript +listProjects(): Promise +``` +Lists all accessible projects. + +#### Get Project +```typescript +getProject({ name: string }): Promise +``` +Gets details of a specific project. + +### Repositories + +#### List Repositories +```typescript +listRepositories({ project: string }): Promise +``` +Lists all repositories in a project. + +#### Create Repository +```typescript +createRepository({ + project: string, + name: string +}): Promise +``` +Creates a new repository in the specified project. + +## Examples + +### Managing Work Items +```typescript +// Get a work item +const workItem = await azure.getWorkItem({ id: 123 }); + +// Create a new task +const newTask = await azure.createWorkItem({ + project: 'MyProject', + type: 'Task', + title: 'Implement feature X', + description: 'Implementation details...' +}); + +// Update work item state +const updatedItem = await azure.updateWorkItem({ + id: 123, + state: 'Active' +}); +``` + +### Managing Projects +```typescript +// List all projects +const projects = await azure.listProjects(); + +// Get specific project +const project = await azure.getProject({ name: 'MyProject' }); +``` + +### Managing Repositories +```typescript +// List repositories +const repos = await azure.listRepositories({ project: 'MyProject' }); + +// Create new repository +const newRepo = await azure.createRepository({ + project: 'MyProject', + name: 'new-service' +}); +``` + +## Development + +```bash +# Install dependencies +npm install + +# Run tests +npm test + +# Build +npm run build + +# Lint +npm run lint +``` + +## Contributing + +See [CONTRIBUTING.md](../../CONTRIBUTING.md) for information about contributing to this repository. + +## License + +This project is licensed under the MIT License - see the [LICENSE](../../LICENSE) file for details. \ No newline at end of file diff --git a/src/azure-devops/examples/basic-usage.ts b/src/azure-devops/examples/basic-usage.ts new file mode 100644 index 00000000..290f1747 --- /dev/null +++ b/src/azure-devops/examples/basic-usage.ts @@ -0,0 +1,35 @@ +import { AzureDevOpsMCPServer } from '../index'; + +async function main() { + // Initialize the server + const server = new AzureDevOpsMCPServer(); + + try { + // List all projects + const projects = await server.projects.listProjects(); + console.log('Projects:', projects); + + // Create a work item + const workItem = await server.workItems.createWorkItem({ + project: 'Your Project Name', + type: 'Task', + title: 'Example Task', + description: 'This is an example task created through the MCP server' + }); + console.log('Created Work Item:', workItem); + + // List repositories + const repos = await server.repositories.listRepositories({ + project: 'Your Project Name' + }); + console.log('Repositories:', repos); + + } catch (error) { + console.error('Error:', error.message); + } +} + +// Run the example if this script is executed directly +if (require.main === module) { + main().catch(console.error); +} \ No newline at end of file diff --git a/src/azure-devops/functions/projects.ts b/src/azure-devops/functions/projects.ts new file mode 100644 index 00000000..8ebba87f --- /dev/null +++ b/src/azure-devops/functions/projects.ts @@ -0,0 +1,106 @@ +import { MCPFunction, MCPFunctionGroup } from '@modelcontextprotocol/typescript-sdk'; +import * as azdev from 'azure-devops-node-api'; +import { Project } from '../types'; + +export class ProjectManagement implements MCPFunctionGroup { + private connection: azdev.WebApi; + + constructor() { + const orgUrl = process.env.AZURE_DEVOPS_ORG_URL; + const token = process.env.AZURE_PERSONAL_ACCESS_TOKEN; + + if (!orgUrl || !token) { + throw new Error('Azure DevOps organization URL and PAT must be provided'); + } + + const authHandler = azdev.getPersonalAccessTokenHandler(token); + this.connection = new azdev.WebApi(orgUrl, authHandler); + } + + @MCPFunction({ + description: 'List all projects', + parameters: { + type: 'object', + properties: {} + } + }) + async listProjects(): Promise { + const client = await this.connection.getCoreApi(); + const projects = await client.getProjects(); + + return projects.map(project => ({ + id: project.id, + name: project.name, + description: project.description + })); + } + + @MCPFunction({ + description: 'Get project by name', + parameters: { + type: 'object', + properties: { + name: { type: 'string', description: 'Project name' } + }, + required: ['name'] + } + }) + async getProject({ name }: { name: string }): Promise { + const client = await this.connection.getCoreApi(); + const project = await client.getProject(name); + + return { + id: project.id, + name: project.name, + description: project.description + }; + } + + @MCPFunction({ + description: 'Create new project', + parameters: { + type: 'object', + properties: { + name: { type: 'string', description: 'Project name' }, + description: { type: 'string', description: 'Project description' } + }, + required: ['name'] + } + }) + async createProject({ name, description }: { + name: string; + description?: string; + }): Promise { + const client = await this.connection.getCoreApi(); + + const project = await client.createProject({ + name, + description, + capabilities: { + versioncontrol: { sourceControlType: 'Git' }, + processTemplate: { templateTypeId: '6b724908-ef14-45cf-84f8-768b5384da45' } // Basic process + } + }); + + return { + id: project.id, + name: project.name, + description: project.description + }; + } + + @MCPFunction({ + description: 'Delete project', + parameters: { + type: 'object', + properties: { + name: { type: 'string', description: 'Project name' } + }, + required: ['name'] + } + }) + async deleteProject({ name }: { name: string }): Promise { + const client = await this.connection.getCoreApi(); + await client.deleteProject(name); + } +} \ No newline at end of file diff --git a/src/azure-devops/functions/repositories.ts b/src/azure-devops/functions/repositories.ts new file mode 100644 index 00000000..2c68631c --- /dev/null +++ b/src/azure-devops/functions/repositories.ts @@ -0,0 +1,124 @@ +import { MCPFunction, MCPFunctionGroup } from '@modelcontextprotocol/typescript-sdk'; +import * as azdev from 'azure-devops-node-api'; +import { Repository } from '../types'; + +export class RepositoryManagement implements MCPFunctionGroup { + private connection: azdev.WebApi; + + constructor() { + const orgUrl = process.env.AZURE_DEVOPS_ORG_URL; + const token = process.env.AZURE_PERSONAL_ACCESS_TOKEN; + + if (!orgUrl || !token) { + throw new Error('Azure DevOps organization URL and PAT must be provided'); + } + + const authHandler = azdev.getPersonalAccessTokenHandler(token); + this.connection = new azdev.WebApi(orgUrl, authHandler); + } + + @MCPFunction({ + description: 'List repositories in a project', + parameters: { + type: 'object', + properties: { + project: { type: 'string', description: 'Project name' } + }, + required: ['project'] + } + }) + async listRepositories({ project }: { project: string }): Promise { + const client = await this.connection.getGitApi(); + const repos = await client.getRepositories(project); + + return repos.map(repo => ({ + id: repo.id, + name: repo.name, + defaultBranch: repo.defaultBranch, + url: repo.url + })); + } + + @MCPFunction({ + description: 'Create new repository', + parameters: { + type: 'object', + properties: { + project: { type: 'string', description: 'Project name' }, + name: { type: 'string', description: 'Repository name' } + }, + required: ['project', 'name'] + } + }) + async createRepository({ project, name }: { + project: string; + name: string; + }): Promise { + const client = await this.connection.getGitApi(); + + const repo = await client.createRepository({ + name, + project: { name: project } + }); + + return { + id: repo.id, + name: repo.name, + defaultBranch: repo.defaultBranch, + url: repo.url + }; + } + + @MCPFunction({ + description: 'Delete repository', + parameters: { + type: 'object', + properties: { + project: { type: 'string', description: 'Project name' }, + name: { type: 'string', description: 'Repository name' } + }, + required: ['project', 'name'] + } + }) + async deleteRepository({ project, name }: { + project: string; + name: string; + }): Promise { + const client = await this.connection.getGitApi(); + const repos = await client.getRepositories(project); + const repo = repos.find(r => r.name === name); + + if (!repo) { + throw new Error(`Repository ${name} not found in project ${project}`); + } + + await client.deleteRepository(repo.id); + } + + @MCPFunction({ + description: 'Get repository branches', + parameters: { + type: 'object', + properties: { + project: { type: 'string', description: 'Project name' }, + repository: { type: 'string', description: 'Repository name' } + }, + required: ['project', 'repository'] + } + }) + async getBranches({ project, repository }: { + project: string; + repository: string; + }): Promise { + const client = await this.connection.getGitApi(); + const repos = await client.getRepositories(project); + const repo = repos.find(r => r.name === repository); + + if (!repo) { + throw new Error(`Repository ${repository} not found in project ${project}`); + } + + const branches = await client.getBranches(repo.id); + return branches.map(branch => branch.name); + } +} \ No newline at end of file diff --git a/src/azure-devops/functions/workItems.ts b/src/azure-devops/functions/workItems.ts new file mode 100644 index 00000000..dde8c2f6 --- /dev/null +++ b/src/azure-devops/functions/workItems.ts @@ -0,0 +1,188 @@ +import { MCPFunction, MCPFunctionGroup } from '@modelcontextprotocol/typescript-sdk'; +import * as azdev from 'azure-devops-node-api'; +import { WorkItem } from '../types'; + +/** + * WorkItemManagement class handles all work item related operations in Azure DevOps + * through the Model Context Protocol interface. + * + * @implements {MCPFunctionGroup} + */ +export class WorkItemManagement implements MCPFunctionGroup { + private connection: azdev.WebApi; + + /** + * Initializes the WorkItemManagement with Azure DevOps credentials + * @throws {Error} If required environment variables are not set + */ + constructor() { + const orgUrl = process.env.AZURE_DEVOPS_ORG_URL; + const token = process.env.AZURE_PERSONAL_ACCESS_TOKEN; + + if (!orgUrl || !token) { + throw new Error('Azure DevOps organization URL and PAT must be provided'); + } + + const authHandler = azdev.getPersonalAccessTokenHandler(token); + this.connection = new azdev.WebApi(orgUrl, authHandler); + } + + /** + * Retrieves a work item by its ID + * + * @param {Object} params - Parameters for the function + * @param {number} params.id - The ID of the work item to retrieve + * @returns {Promise} The requested work item + * @throws {Error} If the work item is not found or access is denied + */ + @MCPFunction({ + description: 'Get work item by ID', + parameters: { + type: 'object', + properties: { + id: { type: 'number', description: 'Work item ID' } + }, + required: ['id'] + } + }) + async getWorkItem({ id }: { id: number }): Promise { + try { + const client = await this.connection.getWorkItemTrackingApi(); + const item = await client.getWorkItem(id); + + if (!item) { + throw new Error(`Work item ${id} not found`); + } + + return { + id: item.id, + title: item.fields['System.Title'], + state: item.fields['System.State'], + type: item.fields['System.WorkItemType'], + description: item.fields['System.Description'] + }; + } catch (error) { + throw new Error(`Failed to get work item ${id}: ${error.message}`); + } + } + + /** + * Creates a new work item + * + * @param {Object} params - Parameters for creating the work item + * @param {string} params.project - The project where the work item will be created + * @param {string} params.type - The type of work item (e.g., Bug, Task, User Story) + * @param {string} params.title - The title of the work item + * @param {string} [params.description] - Optional description for the work item + * @returns {Promise} The created work item + * @throws {Error} If creation fails or parameters are invalid + */ + @MCPFunction({ + description: 'Create new work item', + parameters: { + type: 'object', + properties: { + project: { type: 'string', description: 'Project name' }, + type: { type: 'string', description: 'Work item type (e.g., Bug, Task, User Story)' }, + title: { type: 'string', description: 'Work item title' }, + description: { type: 'string', description: 'Work item description' } + }, + required: ['project', 'type', 'title'] + } + }) + async createWorkItem({ project, type, title, description }: { + project: string; + type: string; + title: string; + description?: string; + }): Promise { + try { + const client = await this.connection.getWorkItemTrackingApi(); + + const patchDocument = [ + { op: 'add', path: '/fields/System.Title', value: title } + ]; + + if (description) { + patchDocument.push({ op: 'add', path: '/fields/System.Description', value: description }); + } + + const item = await client.createWorkItem( + null, + patchDocument, + project, + type + ); + + return { + id: item.id, + title: item.fields['System.Title'], + state: item.fields['System.State'], + type: item.fields['System.WorkItemType'], + description: item.fields['System.Description'] + }; + } catch (error) { + throw new Error(`Failed to create work item: ${error.message}`); + } + } + + /** + * Updates an existing work item + * + * @param {Object} params - Parameters for updating the work item + * @param {number} params.id - The ID of the work item to update + * @param {string} [params.title] - New title for the work item + * @param {string} [params.state] - New state for the work item + * @param {string} [params.description] - New description for the work item + * @returns {Promise} The updated work item + * @throws {Error} If update fails or work item is not found + */ + @MCPFunction({ + description: 'Update work item', + parameters: { + type: 'object', + properties: { + id: { type: 'number', description: 'Work item ID' }, + title: { type: 'string', description: 'New title' }, + state: { type: 'string', description: 'New state' }, + description: { type: 'string', description: 'New description' } + }, + required: ['id'] + } + }) + async updateWorkItem({ id, title, state, description }: { + id: number; + title?: string; + state?: string; + description?: string; + }): Promise { + try { + const client = await this.connection.getWorkItemTrackingApi(); + + const patchDocument = []; + if (title) patchDocument.push({ op: 'add', path: '/fields/System.Title', value: title }); + if (state) patchDocument.push({ op: 'add', path: '/fields/System.State', value: state }); + if (description) patchDocument.push({ op: 'add', path: '/fields/System.Description', value: description }); + + if (patchDocument.length === 0) { + throw new Error('No updates specified'); + } + + const item = await client.updateWorkItem( + null, + patchDocument, + id + ); + + return { + id: item.id, + title: item.fields['System.Title'], + state: item.fields['System.State'], + type: item.fields['System.WorkItemType'], + description: item.fields['System.Description'] + }; + } catch (error) { + throw new Error(`Failed to update work item ${id}: ${error.message}`); + } + } +} \ No newline at end of file diff --git a/src/azure-devops/index.ts b/src/azure-devops/index.ts new file mode 100644 index 00000000..f4f5ed93 --- /dev/null +++ b/src/azure-devops/index.ts @@ -0,0 +1,21 @@ +import { MCPServer } from '@modelcontextprotocol/typescript-sdk'; +import { WorkItemManagement } from './functions/workItems'; +import { ProjectManagement } from './functions/projects'; +import { RepositoryManagement } from './functions/repositories'; + +export class AzureDevOpsMCPServer extends MCPServer { + constructor() { + super(); + + // Register function groups + this.registerFunctions(new WorkItemManagement()); + this.registerFunctions(new ProjectManagement()); + this.registerFunctions(new RepositoryManagement()); + } +} + +// Start server if run directly +if (require.main === module) { + const server = new AzureDevOpsMCPServer(); + server.start(); +} \ No newline at end of file diff --git a/src/azure-devops/jest.config.js b/src/azure-devops/jest.config.js new file mode 100644 index 00000000..b3d946e6 --- /dev/null +++ b/src/azure-devops/jest.config.js @@ -0,0 +1,23 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/tests'], + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest' + }, + coverageThreshold: { + global: { + branches: 80, + functions: 80, + lines: 80, + statements: 80 + } + }, + collectCoverageFrom: [ + 'functions/**/*.ts', + 'types/**/*.ts', + 'index.ts', + '!tests/**/*' + ] +}; \ No newline at end of file diff --git a/src/azure-devops/package.json b/src/azure-devops/package.json new file mode 100644 index 00000000..a849572a --- /dev/null +++ b/src/azure-devops/package.json @@ -0,0 +1,47 @@ +{ + "name": "@modelcontextprotocol/server-azure-devops", + "version": "0.1.0", + "description": "Azure DevOps MCP Server Implementation", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "test": "jest --coverage", + "lint": "eslint . --ext .ts", + "format": "prettier --write \"**/*.ts\"", + "prepare": "npm run build", + "start": "node dist/index.js" + }, + "dependencies": { + "@modelcontextprotocol/typescript-sdk": "^0.1.0", + "azure-devops-node-api": "^12.0.0" + }, + "devDependencies": { + "@types/jest": "^29.5.0", + "@types/node": "^18.0.0", + "@typescript-eslint/eslint-plugin": "^5.0.0", + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^8.0.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.5.0", + "prettier": "^2.8.0", + "ts-jest": "^29.1.0", + "typescript": "^5.0.0" + }, + "publishConfig": { + "access": "public" + }, + "keywords": [ + "mcp", + "azure-devops", + "modelcontextprotocol" + ], + "author": "Zubeid Hendricks", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/modelcontextprotocol/servers.git", + "directory": "src/azure-devops" + } +} \ No newline at end of file diff --git a/src/azure-devops/tests/integration/basic.test.ts b/src/azure-devops/tests/integration/basic.test.ts new file mode 100644 index 00000000..2ee8c8a0 --- /dev/null +++ b/src/azure-devops/tests/integration/basic.test.ts @@ -0,0 +1,67 @@ +import { AzureDevOpsMCPServer } from '../../index'; + +describe('Azure DevOps MCP Server Integration', () => { + let server: AzureDevOpsMCPServer; + + beforeAll(() => { + // These would typically be set in the environment or a .env file + process.env.AZURE_DEVOPS_ORG_URL = process.env.TEST_AZURE_DEVOPS_ORG_URL; + process.env.AZURE_PERSONAL_ACCESS_TOKEN = process.env.TEST_AZURE_PERSONAL_ACCESS_TOKEN; + }); + + beforeEach(() => { + server = new AzureDevOpsMCPServer(); + }); + + it('should initialize without errors', () => { + expect(server).toBeInstanceOf(AzureDevOpsMCPServer); + }); + + describe('when credentials are not provided', () => { + beforeEach(() => { + delete process.env.AZURE_DEVOPS_ORG_URL; + delete process.env.AZURE_PERSONAL_ACCESS_TOKEN; + }); + + it('should throw an error', () => { + expect(() => new AzureDevOpsMCPServer()).toThrow(); + }); + }); + + describe('Work Items', () => { + it('should create and retrieve work items', async () => { + const workItemTitle = `Test Item ${Date.now()}`; + + // Create work item + const created = await server.workItems.createWorkItem({ + project: process.env.TEST_PROJECT_NAME || 'Test Project', + type: 'Task', + title: workItemTitle, + description: 'Test description' + }); + + expect(created.title).toBe(workItemTitle); + + // Retrieve work item + const retrieved = await server.workItems.getWorkItem({ id: created.id }); + expect(retrieved.id).toBe(created.id); + expect(retrieved.title).toBe(workItemTitle); + }); + }); + + describe('Projects', () => { + it('should list projects', async () => { + const projects = await server.projects.listProjects(); + expect(Array.isArray(projects)).toBe(true); + }); + }); + + describe('Repositories', () => { + it('should list repositories in a project', async () => { + const repos = await server.repositories.listRepositories({ + project: process.env.TEST_PROJECT_NAME || 'Test Project' + }); + expect(Array.isArray(repos)).toBe(true); + }); + }); +}); \ No newline at end of file diff --git a/src/azure-devops/tests/projects.test.ts b/src/azure-devops/tests/projects.test.ts new file mode 100644 index 00000000..dd5894af --- /dev/null +++ b/src/azure-devops/tests/projects.test.ts @@ -0,0 +1,88 @@ +import { ProjectManagement } from '../functions/projects'; +import * as azdev from 'azure-devops-node-api'; + +jest.mock('azure-devops-node-api'); + +describe('ProjectManagement', () => { + let projectManager: ProjectManagement; + const mockCoreApi = { + getProjects: jest.fn(), + getProject: jest.fn(), + createProject: jest.fn(), + deleteProject: jest.fn() + }; + + beforeEach(() => { + process.env.AZURE_DEVOPS_ORG_URL = 'https://dev.azure.com/test'; + process.env.AZURE_PERSONAL_ACCESS_TOKEN = 'test-token'; + + (azdev.WebApi as jest.Mock).mockImplementation(() => ({ + getCoreApi: () => mockCoreApi + })); + + projectManager = new ProjectManagement(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('listProjects', () => { + it('should list all projects', async () => { + const mockProjects = [ + { id: '1', name: 'Project 1', description: 'Description 1' }, + { id: '2', name: 'Project 2', description: 'Description 2' } + ]; + + mockCoreApi.getProjects.mockResolvedValue(mockProjects); + + const result = await projectManager.listProjects(); + + expect(result).toEqual(mockProjects); + expect(mockCoreApi.getProjects).toHaveBeenCalled(); + }); + + it('should handle errors when listing projects', async () => { + mockCoreApi.getProjects.mockRejectedValue(new Error('API Error')); + + await expect(projectManager.listProjects()).rejects.toThrow('Failed to list projects'); + }); + }); + + describe('getProject', () => { + it('should get project by name', async () => { + const mockProject = { + id: '1', + name: 'Test Project', + description: 'Test Description' + }; + + mockCoreApi.getProject.mockResolvedValue(mockProject); + + const result = await projectManager.getProject({ name: 'Test Project' }); + + expect(result).toEqual(mockProject); + expect(mockCoreApi.getProject).toHaveBeenCalledWith('Test Project'); + }); + }); + + describe('createProject', () => { + it('should create a new project', async () => { + const mockProject = { + id: '1', + name: 'New Project', + description: 'New Description' + }; + + mockCoreApi.createProject.mockResolvedValue(mockProject); + + const result = await projectManager.createProject({ + name: 'New Project', + description: 'New Description' + }); + + expect(result).toEqual(mockProject); + expect(mockCoreApi.createProject).toHaveBeenCalled(); + }); + }); +}); \ No newline at end of file diff --git a/src/azure-devops/tests/repositories.test.ts b/src/azure-devops/tests/repositories.test.ts new file mode 100644 index 00000000..6de5cd0d --- /dev/null +++ b/src/azure-devops/tests/repositories.test.ts @@ -0,0 +1,103 @@ +import { RepositoryManagement } from '../functions/repositories'; +import * as azdev from 'azure-devops-node-api'; + +jest.mock('azure-devops-node-api'); + +describe('RepositoryManagement', () => { + let repoManager: RepositoryManagement; + const mockGitApi = { + getRepositories: jest.fn(), + createRepository: jest.fn(), + deleteRepository: jest.fn(), + getBranches: jest.fn() + }; + + beforeEach(() => { + process.env.AZURE_DEVOPS_ORG_URL = 'https://dev.azure.com/test'; + process.env.AZURE_PERSONAL_ACCESS_TOKEN = 'test-token'; + + (azdev.WebApi as jest.Mock).mockImplementation(() => ({ + getGitApi: () => mockGitApi + })); + + repoManager = new RepositoryManagement(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('listRepositories', () => { + it('should list repositories in a project', async () => { + const mockRepos = [ + { + id: '1', + name: 'Repo 1', + defaultBranch: 'main', + url: 'https://example.com/repo1' + }, + { + id: '2', + name: 'Repo 2', + defaultBranch: 'main', + url: 'https://example.com/repo2' + } + ]; + + mockGitApi.getRepositories.mockResolvedValue(mockRepos); + + const result = await repoManager.listRepositories({ project: 'Test Project' }); + + expect(result).toEqual(mockRepos); + expect(mockGitApi.getRepositories).toHaveBeenCalledWith('Test Project'); + }); + }); + + describe('createRepository', () => { + it('should create a new repository', async () => { + const mockRepo = { + id: '1', + name: 'New Repo', + defaultBranch: 'main', + url: 'https://example.com/new-repo' + }; + + mockGitApi.createRepository.mockResolvedValue(mockRepo); + + const result = await repoManager.createRepository({ + project: 'Test Project', + name: 'New Repo' + }); + + expect(result).toEqual(mockRepo); + expect(mockGitApi.createRepository).toHaveBeenCalledWith({ + name: 'New Repo', + project: { name: 'Test Project' } + }); + }); + }); + + describe('getBranches', () => { + it('should get repository branches', async () => { + const mockRepos = [{ + id: '1', + name: 'Test Repo' + }]; + const mockBranches = [ + { name: 'main' }, + { name: 'develop' } + ]; + + mockGitApi.getRepositories.mockResolvedValue(mockRepos); + mockGitApi.getBranches.mockResolvedValue(mockBranches); + + const result = await repoManager.getBranches({ + project: 'Test Project', + repository: 'Test Repo' + }); + + expect(result).toEqual(['main', 'develop']); + expect(mockGitApi.getBranches).toHaveBeenCalledWith('1'); + }); + }); +}); \ No newline at end of file diff --git a/src/azure-devops/tests/workItems.test.ts b/src/azure-devops/tests/workItems.test.ts new file mode 100644 index 00000000..180ef92b --- /dev/null +++ b/src/azure-devops/tests/workItems.test.ts @@ -0,0 +1,50 @@ +import { WorkItemManagement } from '../functions/workItems'; +import * as azdev from 'azure-devops-node-api'; + +jest.mock('azure-devops-node-api'); + +describe('WorkItemManagement', () => { + let workItemManager: WorkItemManagement; + + beforeEach(() => { + process.env.AZURE_DEVOPS_ORG_URL = 'https://dev.azure.com/test'; + process.env.AZURE_PERSONAL_ACCESS_TOKEN = 'test-token'; + workItemManager = new WorkItemManagement(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('getWorkItem', () => { + it('should retrieve work item by ID', async () => { + const mockWorkItem = { + id: 1, + fields: { + 'System.Title': 'Test Work Item', + 'System.State': 'Active', + 'System.WorkItemType': 'Task', + 'System.Description': 'Test Description' + } + }; + + const mockClient = { + getWorkItem: jest.fn().mockResolvedValue(mockWorkItem) + }; + + (azdev.WebApi as jest.Mock).mockImplementation(() => ({ + getWorkItemTrackingApi: () => mockClient + })); + + const result = await workItemManager.getWorkItem({ id: 1 }); + + expect(result).toEqual({ + id: 1, + title: 'Test Work Item', + state: 'Active', + type: 'Task', + description: 'Test Description' + }); + }); + }); +}); \ No newline at end of file diff --git a/src/azure-devops/tsconfig.json b/src/azure-devops/tsconfig.json new file mode 100644 index 00000000..552f598b --- /dev/null +++ b/src/azure-devops/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "declaration": true, + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "rootDir": "./", + "baseUrl": "./", + "paths": { + "*": ["node_modules/*", "src/types/*"] + } + }, + "include": [ + "**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts" + ] +} \ No newline at end of file diff --git a/src/azure-devops/types/index.ts b/src/azure-devops/types/index.ts new file mode 100644 index 00000000..cf4409b9 --- /dev/null +++ b/src/azure-devops/types/index.ts @@ -0,0 +1,20 @@ +export interface WorkItem { + id: number; + title: string; + state: string; + type: string; + description?: string; +} + +export interface Project { + id: string; + name: string; + description?: string; +} + +export interface Repository { + id: string; + name: string; + defaultBranch: string; + url: string; +} \ No newline at end of file diff --git a/test/index.test.ts b/test/index.test.ts new file mode 100644 index 00000000..bb6f2a32 --- /dev/null +++ b/test/index.test.ts @@ -0,0 +1,17 @@ +import { AzureDevOpsMCPServer } from '../src'; + +describe('AzureDevOpsMCPServer', () => { + let server: AzureDevOpsMCPServer; + + beforeEach(() => { + process.env.AZURE_DEVOPS_ORG_URL = 'https://dev.azure.com/testorg'; + process.env.AZURE_PERSONAL_ACCESS_TOKEN = 'test-token'; + server = new AzureDevOpsMCPServer(); + }); + + it('should initialize without errors', () => { + expect(server).toBeInstanceOf(AzureDevOpsMCPServer); + }); + + // Add more tests for each function group +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 208ca01e..9ab78c9e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,14 +1,14 @@ { "compilerOptions": { - "target": "ES2022", - "module": "Node16", - "moduleResolution": "Node16", + "target": "ES2019", + "module": "commonjs", + "declaration": true, + "outDir": "./dist", "strict": true, "esModuleInterop": true, "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true + "forceConsistentCasingInFileNames": true }, - "include": ["src/**/*"], - "exclude": ["node_modules"] -} + "include": ["src"], + "exclude": ["node_modules", "dist", "test"] +} \ No newline at end of file