Skip to main content
Glama

Azure DevOps MCP Server with PAT Authentication

by ennuiii
wiki.ts10.6 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import { AccessToken } from "@azure/identity"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { WebApi } from "azure-devops-node-api"; import { z } from "zod"; import { WikiPagesBatchRequest } from "azure-devops-node-api/interfaces/WikiInterfaces.js"; const WIKI_TOOLS = { list_wikis: "wiki_list_wikis", get_wiki: "wiki_get_wiki", list_wiki_pages: "wiki_list_pages", get_wiki_page_content: "wiki_get_page_content", create_or_update_page: "wiki_create_or_update_page", }; function configureWikiTools(server: McpServer, tokenProvider: () => Promise<AccessToken>, connectionProvider: () => Promise<WebApi>) { server.tool( WIKI_TOOLS.get_wiki, "Get the wiki by wikiIdentifier", { wikiIdentifier: z.string().describe("The unique identifier of the wiki."), project: z.string().optional().describe("The project name or ID where the wiki is located. If not provided, the default project will be used."), }, async ({ wikiIdentifier, project }) => { try { const connection = await connectionProvider(); const wikiApi = await connection.getWikiApi(); const wiki = await wikiApi.getWiki(wikiIdentifier, project); if (!wiki) { return { content: [{ type: "text", text: "No wiki found" }], isError: true }; } return { content: [{ type: "text", text: JSON.stringify(wiki, null, 2) }], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; return { content: [{ type: "text", text: `Error fetching wiki: ${errorMessage}` }], isError: true, }; } } ); server.tool( WIKI_TOOLS.list_wikis, "Retrieve a list of wikis for an organization or project.", { project: z.string().optional().describe("The project name or ID to filter wikis. If not provided, all wikis in the organization will be returned."), }, async ({ project }) => { try { const connection = await connectionProvider(); const wikiApi = await connection.getWikiApi(); const wikis = await wikiApi.getAllWikis(project); if (!wikis) { return { content: [{ type: "text", text: "No wikis found" }], isError: true }; } return { content: [{ type: "text", text: JSON.stringify(wikis, null, 2) }], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; return { content: [{ type: "text", text: `Error fetching wikis: ${errorMessage}` }], isError: true, }; } } ); server.tool( WIKI_TOOLS.list_wiki_pages, "Retrieve a list of wiki pages for a specific wiki and project.", { wikiIdentifier: z.string().describe("The unique identifier of the wiki."), project: z.string().describe("The project name or ID where the wiki is located."), top: z.number().default(20).describe("The maximum number of pages to return. Defaults to 20."), continuationToken: z.string().optional().describe("Token for pagination to retrieve the next set of pages."), pageViewsForDays: z.number().optional().describe("Number of days to retrieve page views for. If not specified, page views are not included."), }, async ({ wikiIdentifier, project, top = 20, continuationToken, pageViewsForDays }) => { try { const connection = await connectionProvider(); const wikiApi = await connection.getWikiApi(); const pagesBatchRequest: WikiPagesBatchRequest = { top, continuationToken, pageViewsForDays, }; const pages = await wikiApi.getPagesBatch(pagesBatchRequest, project, wikiIdentifier); if (!pages) { return { content: [{ type: "text", text: "No wiki pages found" }], isError: true }; } return { content: [{ type: "text", text: JSON.stringify(pages, null, 2) }], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; return { content: [{ type: "text", text: `Error fetching wiki pages: ${errorMessage}` }], isError: true, }; } } ); server.tool( WIKI_TOOLS.get_wiki_page_content, "Retrieve wiki page content by wikiIdentifier and path.", { wikiIdentifier: z.string().describe("The unique identifier of the wiki."), project: z.string().describe("The project name or ID where the wiki is located."), path: z.string().describe("The path of the wiki page to retrieve content for."), }, async ({ wikiIdentifier, project, path }) => { try { const connection = await connectionProvider(); const wikiApi = await connection.getWikiApi(); const stream = await wikiApi.getPageText(project, wikiIdentifier, path, undefined, undefined, true); if (!stream) { return { content: [{ type: "text", text: "No wiki page content found" }], isError: true }; } const content = await streamToString(stream); return { content: [{ type: "text", text: JSON.stringify(content, null, 2) }], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; return { content: [{ type: "text", text: `Error fetching wiki page content: ${errorMessage}` }], isError: true, }; } } ); server.tool( WIKI_TOOLS.create_or_update_page, "Create or update a wiki page with content.", { wikiIdentifier: z.string().describe("The unique identifier or name of the wiki."), path: z.string().describe("The path of the wiki page (e.g., '/Home' or '/Documentation/Setup')."), content: z.string().describe("The content of the wiki page in markdown format."), project: z.string().optional().describe("The project name or ID where the wiki is located. If not provided, the default project will be used."), etag: z.string().optional().describe("ETag for editing existing pages (optional, will be fetched if not provided)."), }, async ({ wikiIdentifier, path, content, project, etag }) => { try { const connection = await connectionProvider(); const accessToken = await tokenProvider(); // Normalize the path const normalizedPath = path.startsWith("/") ? path : `/${path}`; const encodedPath = encodeURIComponent(normalizedPath); // Build the URL for the wiki page API const baseUrl = connection.serverUrl; const projectParam = project || ""; const url = `${baseUrl}/${projectParam}/_apis/wiki/wikis/${wikiIdentifier}/pages?path=${encodedPath}&api-version=7.1`; // First, try to create a new page (PUT without ETag) try { const createResponse = await fetch(url, { method: "PUT", headers: { "Authorization": `Bearer ${accessToken.token}`, "Content-Type": "application/json", }, body: JSON.stringify({ content: content }), }); if (createResponse.ok) { const result = await createResponse.json(); return { content: [ { type: "text", text: `Successfully created wiki page at path: ${normalizedPath}. Response: ${JSON.stringify(result, null, 2)}`, }, ], }; } // If creation failed with 409 (Conflict) or 500 (Page exists), try to update it if (createResponse.status === 409 || createResponse.status === 500) { // Page exists, we need to get the ETag and update it let currentEtag = etag; if (!currentEtag) { // Fetch current page to get ETag const getResponse = await fetch(url, { method: "GET", headers: { Authorization: `Bearer ${accessToken.token}`, }, }); if (getResponse.ok) { currentEtag = getResponse.headers.get("etag") || getResponse.headers.get("ETag") || undefined; if (!currentEtag) { const pageData = await getResponse.json(); currentEtag = pageData.eTag; } } if (!currentEtag) { throw new Error("Could not retrieve ETag for existing page"); } } // Now update the existing page with ETag const updateResponse = await fetch(url, { method: "PUT", headers: { "Authorization": `Bearer ${accessToken.token}`, "Content-Type": "application/json", "If-Match": currentEtag, }, body: JSON.stringify({ content: content }), }); if (updateResponse.ok) { const result = await updateResponse.json(); return { content: [ { type: "text", text: `Successfully updated wiki page at path: ${normalizedPath}. Response: ${JSON.stringify(result, null, 2)}`, }, ], }; } else { const errorText = await updateResponse.text(); throw new Error(`Failed to update page (${updateResponse.status}): ${errorText}`); } } else { const errorText = await createResponse.text(); throw new Error(`Failed to create page (${createResponse.status}): ${errorText}`); } } catch (fetchError) { throw fetchError; } } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; return { content: [{ type: "text", text: `Error creating/updating wiki page: ${errorMessage}` }], isError: true, }; } } ); } function streamToString(stream: NodeJS.ReadableStream): Promise<string> { return new Promise((resolve, reject) => { let data = ""; stream.setEncoding("utf8"); stream.on("data", (chunk) => (data += chunk)); stream.on("end", () => resolve(data)); stream.on("error", reject); }); } export { WIKI_TOOLS, configureWikiTools };

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ennuiii/DevOpsMcpPAT'

If you have feedback or need assistance with the MCP directory API, please join our Discord server