Skip to main content
Glama
K3roru

tfs-mcp-server

by K3roru
README.md
# tfs-mcp-server

MCP server (stdio) for **Microsoft TFS / Azure DevOps Server** using REST API **6.0**.
Gives an AI agent tools for work items, Git repository files (read and write: branches, commits, pull requests) and identity lookup.
Work items and repositories may live in different team projects: every tool accepts an
optional `project` argument that overrides the configured default.

## Requirements

- Node.js 20+
- A TFS / Azure DevOps Server collection reachable over HTTPS
- A Personal Access Token with scopes: Work Items (read & write), Code (read & write; write is needed for the Git write tools - branches, commits and pull requests), Identity (read)

## Install & build

```bash
npm install
npm run build        # -> dist/index.js
npm test             # vitest (all TFS calls are mocked)
```

## Configuration (environment variables)

| Variable | Required | Description |
| --- | --- | --- |
| `TFS_BASE_URL` | yes | Collection URL, e.g. `https://tfs.corp.local/tfs/DefaultCollection` |
| `TFS_PAT` | yes | Personal access token (sent as Basic auth) |
| `TFS_DEFAULT_PROJECT` | no | Project used when a call omits `project` |
| `TFS_API_VERSION` | no | Default `6.0` |
| `TFS_BATCH_CONCURRENCY` | no | Parallelism for fan-out batch tools (default `5`) |
| `TFS_INSECURE_TLS` | no | `true` to accept self-signed certificates |

See [.env.example](.env.example).

### Cursor (`~/.cursor/mcp.json` or `.cursor/mcp.json`)

```json
{
  "mcpServers": {
    "tfs": {
      "command": "node",
      "args": ["/absolute/path/to/tfs-mcp-server/dist/index.js"],
      "env": {
        "TFS_BASE_URL": "https://tfs.corp.local/tfs/DefaultCollection",
        "TFS_PAT": "<your PAT>",
        "TFS_DEFAULT_PROJECT": "MyProject"
      }
    }
  }
}
```

Claude Desktop uses the same shape in `claude_desktop_config.json`.

### Interactive testing

```bash
TFS_BASE_URL=... TFS_PAT=... npm run inspect   # opens the MCP Inspector against dist/index.js
```

## Tools

All tools return a short text summary plus a JSON payload (`structuredContent`).
Errors are returned as tool errors with the TFS message, HTTP status and request URL.

### Work items

| Tool | Purpose |
| --- | --- |
| `get_work_item` | Metadata, rich-text content (Description / Repro Steps / Acceptance Criteria, HTML converted to text by default), relations, optional comments |
| `get_work_items` | Same for many ids in one call (`workitemsbatch`, 200 ids per request); missing ids listed in `failed` |
| `get_work_item_comments` | Paged comments (ids needed for updates) |
| `update_work_item` | JSON-Patch update. **`state` and `assignedTo` are mandatory**; optional title, description, tags, area/iteration, priority, arbitrary `fields`, `comment`, `expectedRev`, `bypassRules`, `validateOnly` |
| `update_work_items` | Same update applied to many ids via `wit/$batch` (falls back to parallel PATCH if `$batch` is unavailable); per-item results |
| `add_work_item_comment` | Add a comment (plain text is wrapped into HTML) |
| `add_comment_to_work_items` | Same comment on many work items (bounded-concurrency fan-out) |
| `update_work_item_comment` | Replace the text of an existing comment |
| `get_query` | Saved query by GUID or path: WIQL, columns, type; folders list children |
| `run_query` | Execute a saved query; optional `extraWhere` (raw WIQL condition AND-ed into the query) |
| `run_wiql` | Execute an arbitrary WIQL string as-is |

`assignedTo` accepts a display name, `DOMAIN\user`, an e-mail, `Display Name <DOMAIN\user>`, or `""` to unassign.
Use `get_current_identity` / `search_identities` to obtain valid values.

### Files (Git)

| Tool | Purpose |
| --- | --- |
| `list_repositories` | Repositories of a project (or the whole collection) |
| `get_file_content` | File content at `branch` / `commit` / `tag` (default: repo default branch). Text inline, binaries base64. A missing file produces a verbose diagnosis: does the repository exist, does the branch exist (with a branch list), does the parent directory exist and what does it contain, plus "did you mean" suggestions |
| `get_files_content` | Several files from one repo/version in one call (`itemsbatch` + parallel content download), per-path diagnostics |
| `list_directory` | Entries under a directory (one level or recursive) with object ids, content type and latest commit |

### Git write (branches, commits, pull requests)

| Tool | Purpose |
| --- | --- |
| `create_branch` | Create `name` from `fromBranch` (default: repo default branch) or `fromCommit`. Fails if it exists unless `ifExists: "reuse"` |
| `commit_file_changes` | One commit on an existing `branch` with up to 50 `changes` (`add` / `edit` / `delete`). Each add/edit carries either full `content` (`encoding` utf-8 or base64) or exact find/replace `edits` applied to the current file (each `find` must match exactly once unless `replaceAll`). Optional `expectedBranchTip` guards against concurrent pushes |
| `create_pull_request` | Open `sourceBranch` -> `targetBranch` with title, description, `workItemIds`, `reviewers` (identity ids), `isDraft`. Work items are sent as `workItemRefs`; ids TFS did not record are linked from the work item side with an `ArtifactLink` relation. Per-id link results are returned |
| `create_branch_commit_and_pull_request` | One-shot workflow: create `newBranch` from `targetBranch`, commit `changes`, open the PR (`title` defaults to the commit `message`) linked to `workItemIds`. On failure the error names the failed step, lists completed steps and tells you which granular tool to continue with. `reuseExistingBranch: true` continues on an existing branch |

### Identity

| Tool | Purpose |
| --- | --- |
| `get_current_identity` | Who the PAT belongs to (`connectionData`, enriched via the identities API): id, display name, account, e-mail and a ready-to-use `assignedToValue` |
| `search_identities` | Find users/groups by name, account or e-mail |

## Example flows

```text
get_current_identity                         -> assignedToValue = "Jane Doe <CORP\\jdoe>"
run_query { queryId: "Shared Queries/Team/Triage", extraWhere: "[System.Tags] CONTAINS 'hotfix'" }
update_work_items { ids: [...], state: "Active", assignedTo: "Jane Doe <CORP\\jdoe>" }
add_comment_to_work_items { ids: [...], text: "Picked up in sprint 12" }
get_files_content { project: "Infra", repository: "tools", branch: "develop", paths: ["/README.md", "/src/main.ts"] }
create_branch_commit_and_pull_request {
  project: "Infra", repository: "tools", newBranch: "feature/1234-bump-timeout", targetBranch: "develop",
  message: "Bump request timeout to 60s",
  changes: [{ path: "/src/config.ts", changeType: "edit", edits: [{ find: "timeoutMs: 30_000", replace: "timeoutMs: 60_000" }] }],
  workItemIds: [1234]
}
```

## Notes on TFS API versions

- Work item, WIQL, Git and repository endpoints use `api-version=6.0`.
- The work item **comments** API only exists as a preview in 6.0; the server uses `6.0-preview.3`.
- `connectionData` (`6.0-preview`) and `identities` (`6.0-preview.1`) are collection-scoped; the identities call is optional and failures are tolerated.
- `wit/$batch` is used for bulk updates; if a server rejects it (404/405/400) the tool transparently falls back to individual PATCH requests.
- Branches are created with `POST git/repositories/{repo}/refs` (old object id `000…0`), commits with `POST .../pushes` (one commit, `rawtext` or `base64encoded` content), pull requests with `POST .../pullrequests`. Work items are linked via `workItemRefs` on creation; ids that `GET .../pullRequests/{id}/workitems` does not report afterwards are linked with an `ArtifactLink` relation to `vstfs:///Git/PullRequestId/{projectId}%2F{repositoryId}%2F{pullRequestId}`.
- If TFS answers with an HTML sign-in page or HTTP 203 instead of JSON, the PAT is invalid or the base URL does not point at a collection; the error message says so.

## Project layout

```
src/
  index.ts            stdio entrypoint
  server.ts           McpServer factory + tool registration
  config.ts           env parsing, project resolution
  client.ts           REST client (Basic PAT auth, URL builder, TfsApiError)
  services/           TFS API wrappers (workitems, git (read), gitwrite (branches/pushes/PRs), identity)
  tools/              MCP tool definitions (zod schemas)
  util/               html->text, batch helpers, tool result helpers
tests/                vitest suites with a mocked fetch
```

TDQS

A3.9/5.0

Scored across 17 tools

Disambiguation4/5

Most tools target clearly distinct resources and actions, such as fetching a single work item vs. many, or reading file content vs. listing a directory. The main area of potential confusion is between the singular/plural comment tools, especially add_work_item_comment and add_comment_to_work_items, though their descriptions clarify the difference.

Naming Consistency4/5

Tool names mostly follow a consistent snake_case verb_noun pattern: list_, get_, update_, add_, run_, and search_. Minor deviations exist, notably add_comment_to_work_items vs. add_work_item_comment and get_current_identity, but overall the naming is predictable and readable.

Tool Count4/5

17 tools is slightly above the ideal range but reasonable for a server covering both work item management and Git repository file access. Each tool serves a distinct purpose, and the count reflects the breadth of the domain without feeling bloated.

Completeness3/5

The work item surface is strong for reading and updating, including bulk operations and comments, but there is no create_work_item or delete_work_item, which is a notable lifecycle gap. The Git side is also read-only, covering file/directory access but no branch, commit, or repository management operations.

Maintenance

ActivityMaintained
ResponsivenessNo issues