@zeroheight/mcp-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## Release notes
2
+
3
+ ## [1.0.0](https://www.npmjs.com/package/@zeroheight/mcp-server/v/1.0.0) - 12th August 2025
4
+
5
+ - Create the zeroheight MCP server with support for retrieving styleguide navigation and page content.
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # zeroheight MCP Server
2
+
3
+ MCP server for zeroheight
4
+
5
+ **For a full walkthrough on how to use this MCP server, head over to our [help centre article](https://help.zeroheight.com/hc/en-us/articles/39914754674843-Using-the-zeroheight-MCP-server).**
6
+
7
+ ## Install
8
+
9
+ Add to Claude Desktop or VSCode (including Cursor/Windsurf) config:
10
+
11
+ ```json
12
+ "mcpServers": {
13
+ "zeroheight": {
14
+ "command": "npx",
15
+ "args": ["-y", "@zeroheight/mcp-server@latest"],
16
+ "env": {
17
+ "ZEROHEIGHT_ACCESS_TOKEN": "zhat_abc123",
18
+ "ZEROHEIGHT_CLIENT_ID": "zhci_abc123"
19
+ }
20
+ }
21
+ }
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ### Authentication
27
+
28
+ You will need to add your zeroheight authentication credentials as environment variables, which can be created using [these instructions](https://developers.zeroheight.com/75fe5b2ed/p/6599ef-creation):
29
+
30
+ ```
31
+ ZEROHEIGHT_CLIENT_ID="your-client-id"
32
+ ZEROHEIGHT_ACCESS_TOKEN="your-access-token"
33
+ ```
34
+
35
+ ### Available Tools
36
+
37
+ The server provides these MCP tools:
38
+
39
+ **list-styleguides**
40
+ List all accessible styleguides as resource links or markdown
41
+
42
+ **get-styleguide-tree**
43
+ Get a navigation hierarchy for a styleguide with top-level navigation, categories, pages and tabs
44
+
45
+ **list-pages**
46
+ List all accessible pages in a styleguide with optional search
47
+
48
+ **get-page**
49
+ Get a styleguide page in the requested format (markdown or JSON)
50
+
51
+ ### Setup Examples
52
+
53
+ #### Claude Desktop
54
+
55
+ ```json
56
+ {
57
+ "mcpServers": {
58
+ "zeroheight": {
59
+ "command": "npx",
60
+ "args": ["-y", "@zeroheight/mcp-server@latest"],
61
+ "env": {
62
+ "ZEROHEIGHT_ACCESS_TOKEN": "zhat_abc123",
63
+ "ZEROHEIGHT_CLIENT_ID": "zhci_abc123"
64
+ }
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
70
+ #### VS Code
71
+
72
+ Create `.vscode/mcp.json`:
73
+
74
+ ```json
75
+ {
76
+ "servers": {
77
+ "zeroheight": {
78
+ "command": "npx",
79
+ "args": ["-y", "@zeroheight/mcp-server@latest"],
80
+ "env": {
81
+ "ZEROHEIGHT_ACCESS_TOKEN": "zhat_abc123",
82
+ "ZEROHEIGHT_CLIENT_ID": "zhci_abc123"
83
+ }
84
+ }
85
+ }
86
+ }
87
+ ```
88
+
89
+ ## Example Usage
90
+
91
+ ```
92
+ "List my styleguides"
93
+ "Show pages about buttons"
94
+ "Get the navigation for [styleguide name]"
95
+ "Find documentation about typography"
96
+ ```
97
+
98
+ ## Requirements
99
+
100
+ - Node.js 22+
101
+ - zeroheight Client ID and Access Token
102
+
103
+ More info on setup can be found in the [help centre article](https://help.zeroheight.com/hc/en-us).
@@ -0,0 +1,77 @@
1
+ import { IncorrectScopeError, MaxRetriesError, UnauthorizedError, UnknownError, } from "./errors.js";
2
+ import packageJson from "../../package.json" with { type: "json" };
3
+ const API_PATH = "/open_api/v2";
4
+ const USER_AGENT = `zeroheight-mcp-server/${packageJson.version}`;
5
+ export var ResponseStatus;
6
+ (function (ResponseStatus) {
7
+ ResponseStatus["Success"] = "success";
8
+ ResponseStatus["Error"] = "error";
9
+ ResponseStatus["Fail"] = "fail";
10
+ })(ResponseStatus || (ResponseStatus = {}));
11
+ /**
12
+ * Get the correct URL for production of development depending on the environment variable
13
+ */
14
+ export function getZeroheightURL() {
15
+ if (process.env["NODE_ENV"] === "dev") {
16
+ return new URL("https://zeroheight.dev");
17
+ }
18
+ else {
19
+ return new URL("https://zeroheight.com");
20
+ }
21
+ }
22
+ async function sleep(ms) {
23
+ return new Promise((resolve) => setTimeout(resolve, ms));
24
+ }
25
+ export async function request(path, credentials, init, search) {
26
+ const url = getZeroheightURL();
27
+ url.pathname = API_PATH + path;
28
+ if (search) {
29
+ url.search = "?" + search.toString();
30
+ }
31
+ if (process.env["NODE_ENV"] === "dev") {
32
+ process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
33
+ }
34
+ const maxRetries = 3;
35
+ let retries = 0;
36
+ while (retries < maxRetries) {
37
+ const response = await fetch(url, {
38
+ ...init,
39
+ headers: {
40
+ "X-API-CLIENT-NAME": "mcp-server",
41
+ "X-API-CLIENT": credentials.client,
42
+ "X-API-KEY": credentials.token,
43
+ "X-API-CLIENT-VERSION": packageJson.version,
44
+ "User-Agent": USER_AGENT,
45
+ "Content-Type": "application/json",
46
+ Accept: "application/json",
47
+ },
48
+ });
49
+ if (response.status === 401) {
50
+ console.error("Unauthorized response from API", {
51
+ response: { status: response.status, body: await response.text() },
52
+ });
53
+ throw new UnauthorizedError();
54
+ }
55
+ else if (response.status === 429) {
56
+ retries++;
57
+ const responseData = await response.json();
58
+ const waitTime = responseData.data.reset_time * 1000 - Date.now();
59
+ await sleep(waitTime);
60
+ continue;
61
+ }
62
+ else if (response.status < 200 || response.status >= 300) {
63
+ console.error("API request failed", {
64
+ response: { status: response.status, body: await response.text() },
65
+ });
66
+ if (response.status === 403) {
67
+ throw new IncorrectScopeError();
68
+ }
69
+ else {
70
+ throw new UnknownError();
71
+ }
72
+ }
73
+ const responseJson = await response.json();
74
+ return responseJson;
75
+ }
76
+ throw new MaxRetriesError();
77
+ }
@@ -0,0 +1,12 @@
1
+ import { expect, describe, it } from "@rstest/core";
2
+ import { getZeroheightURL } from "./api.js";
3
+ describe("getZeroheightURL", () => {
4
+ it("uses local development domain when NODE_ENV is dev", () => {
5
+ process.env.NODE_ENV = "dev";
6
+ expect(getZeroheightURL().toString()).toBe("https://zeroheight.dev/");
7
+ });
8
+ it("uses local development domain when NODE_ENV is dev", () => {
9
+ process.env.NODE_ENV = "production";
10
+ expect(getZeroheightURL().toString()).toBe("https://zeroheight.com/");
11
+ });
12
+ });
@@ -0,0 +1,26 @@
1
+ export class ApiError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "ApiError";
5
+ }
6
+ }
7
+ export class UnauthorizedError extends ApiError {
8
+ constructor() {
9
+ super("Unauthorized. Please ensure you have added your credentials.");
10
+ }
11
+ }
12
+ export class IncorrectScopeError extends ApiError {
13
+ constructor() {
14
+ super("The authentication token used does not have the correct scope. \n\nPlease create a new authentication token with the relevant scopes and then update your credentials.");
15
+ }
16
+ }
17
+ export class UnknownError extends ApiError {
18
+ constructor() {
19
+ super("An unknown error occurred while calling the zeroheight API");
20
+ }
21
+ }
22
+ export class MaxRetriesError extends ApiError {
23
+ constructor() {
24
+ super("Max retries exceeded while calling the zeroheight API");
25
+ }
26
+ }
@@ -0,0 +1,46 @@
1
+ import { getCredentialsFromEnvironment } from "../common/credentials.js";
2
+ import { request } from "./api.js";
3
+ import { UnknownError } from "./errors.js";
4
+ export async function listPages(styleguideId, optionalParams = {}) {
5
+ try {
6
+ const credentials = getCredentialsFromEnvironment();
7
+ const searchParams = new URLSearchParams();
8
+ if (optionalParams.releaseId) {
9
+ searchParams.set("release_id", optionalParams.releaseId.toString());
10
+ }
11
+ if (optionalParams.searchTerm) {
12
+ searchParams.set("search", optionalParams.searchTerm);
13
+ }
14
+ const response = await request(`/styleguides/${styleguideId}/pages`, credentials, {
15
+ method: "GET",
16
+ }, searchParams);
17
+ if ("pages" in response.data) {
18
+ return response.data.pages;
19
+ }
20
+ throw new UnknownError();
21
+ }
22
+ catch (e) {
23
+ console.error(e);
24
+ return [];
25
+ }
26
+ }
27
+ export async function getPage(pageId, optionalParams = {}) {
28
+ try {
29
+ const credentials = getCredentialsFromEnvironment();
30
+ const searchParams = new URLSearchParams();
31
+ if (optionalParams.releaseId) {
32
+ searchParams.set("release_id", optionalParams.releaseId.toString());
33
+ }
34
+ const response = await request(`/pages/${pageId}`, credentials, {
35
+ method: "GET",
36
+ }, searchParams);
37
+ if ("page" in response.data) {
38
+ return response.data.page;
39
+ }
40
+ throw new UnknownError();
41
+ }
42
+ catch (e) {
43
+ console.error(e);
44
+ return null;
45
+ }
46
+ }
@@ -0,0 +1,36 @@
1
+ import { request } from "./api.js";
2
+ import { UnknownError } from "./errors.js";
3
+ import { getCredentialsFromEnvironment } from "../common/credentials.js";
4
+ export async function listStyleguides() {
5
+ try {
6
+ const credentials = getCredentialsFromEnvironment();
7
+ const response = await request("/styleguides", credentials, {
8
+ method: "GET",
9
+ });
10
+ if ("styleguides" in response.data) {
11
+ return response.data.styleguides;
12
+ }
13
+ throw new UnknownError();
14
+ }
15
+ catch (e) {
16
+ console.error(e);
17
+ return [];
18
+ }
19
+ }
20
+ export async function getStyleguideTree(id) {
21
+ try {
22
+ const credentials = getCredentialsFromEnvironment();
23
+ const response = await request(`/styleguides/${id}/tree`, credentials, {
24
+ method: "GET",
25
+ });
26
+ if ("tree" in response.data) {
27
+ return response.data.tree;
28
+ }
29
+ console.error(response);
30
+ throw new UnknownError();
31
+ }
32
+ catch (e) {
33
+ console.error(e);
34
+ return [];
35
+ }
36
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import { MissingCredentialsError } from "./errors.js";
2
+ export function getCredentialsFromEnvironment() {
3
+ const token = process.env["ZEROHEIGHT_ACCESS_TOKEN"];
4
+ const client = process.env["ZEROHEIGHT_CLIENT_ID"];
5
+ if (!token || !client) {
6
+ throw new MissingCredentialsError();
7
+ }
8
+ return {
9
+ token,
10
+ client,
11
+ };
12
+ }
@@ -0,0 +1,11 @@
1
+ export class ServerError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "ServerError";
5
+ }
6
+ }
7
+ export class MissingCredentialsError extends ServerError {
8
+ constructor() {
9
+ super("Missing credentials. Please ensure ZEROHEIGHT_CLIENT_ID and ZEROHEIGHT_ACCESS_TOKEN are set in your environment");
10
+ }
11
+ }
@@ -0,0 +1,16 @@
1
+ export function formatPageAsMarkdown(page) {
2
+ const content = page.content ||
3
+ page.tabs?.map((tab) => `## ${tab.name}\n${tab.content}\n---\n`).join("\n");
4
+ return [
5
+ `# ${page.name}`,
6
+ `[Web URL](${page.url})`,
7
+ page.introduction,
8
+ "\n",
9
+ content,
10
+ ].join("\n");
11
+ }
12
+ export function formatPageListItemsAsMarkdown(pages) {
13
+ return pages
14
+ .map((page) => `- ${page.name ?? "Untitled page"} (ID: ${page.id})`)
15
+ .join("\n");
16
+ }
@@ -0,0 +1,25 @@
1
+ export const DEFAULT_NAME = "Unnamed styleguide";
2
+ export function formatTreeAsJson(tree) {
3
+ return (tree?.map((node) => {
4
+ if (node.type === "page" || node.type === "tab") {
5
+ return {
6
+ id: node.id,
7
+ type: node.type,
8
+ name: node.name,
9
+ url: node.url,
10
+ children: formatTreeAsJson(node.children),
11
+ };
12
+ }
13
+ return {
14
+ id: node.id,
15
+ type: node.type,
16
+ name: node.name,
17
+ children: formatTreeAsJson(node.children),
18
+ };
19
+ }) ?? []);
20
+ }
21
+ export function formatStyleguideListAsMarkdown(styleguides) {
22
+ return styleguides
23
+ .map((styleguide) => `- ${styleguide.name ?? DEFAULT_NAME} (ID: ${styleguide.id})`)
24
+ .join("\n");
25
+ }
@@ -0,0 +1,24 @@
1
+ import { expect, describe, it } from "@rstest/core";
2
+ import { formatStyleguideListAsMarkdown } from "./styleguide.js";
3
+ describe("formatStyleguideListAsMarkdown", () => {
4
+ describe("when list is empty", () => {
5
+ it("returns an empty string", () => {
6
+ expect(formatStyleguideListAsMarkdown([])).toBe("");
7
+ });
8
+ });
9
+ describe("when there's one styleguide", () => {
10
+ it("formats styleguide list as markdown list on one line", () => {
11
+ expect(formatStyleguideListAsMarkdown([
12
+ { id: 1, name: "Styleguide 1", team_id: 1, share_id: "abc123" },
13
+ ])).toBe("- Styleguide 1 (ID: 1)");
14
+ });
15
+ });
16
+ describe("when there's multiple styleguides", () => {
17
+ it("formats styleguide list as markdown list", () => {
18
+ expect(formatStyleguideListAsMarkdown([
19
+ { id: 1, name: "Styleguide 1", team_id: 1, share_id: "abc123" },
20
+ { id: 2, name: "Styleguide 2", team_id: 1, share_id: "xyz987" },
21
+ ])).toBe("- Styleguide 1 (ID: 1)\n- Styleguide 2 (ID: 2)");
22
+ });
23
+ });
24
+ });
package/dist/index.js ADDED
@@ -0,0 +1,48 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import packageJson from "../package.json" with { type: "json" };
4
+ import { registerStyleguideListResource, registerStyleguideResource, } from "./resources/styleguide.js";
5
+ import { registerListPagesResource, registerPageResource, } from "./resources/page.js";
6
+ import { registerGetStyleguideTreeTool, registerListStyleguidesTool, } from "./tools/styleguide.js";
7
+ import { registerGetPageTool, registerListPagesTool } from "./tools/page.js";
8
+ import { recommendSummarizePagePrompt } from "./prompts/page.js";
9
+ import { registerRecommendPagesPrompt } from "./prompts/styleguide.js";
10
+ function createServer() {
11
+ // Create server instance
12
+ const server = new McpServer({
13
+ name: "zeroheight",
14
+ version: packageJson.version,
15
+ capabilities: {
16
+ resources: {},
17
+ tools: {},
18
+ },
19
+ });
20
+ // Register all tools
21
+ [
22
+ registerListPagesTool,
23
+ registerGetPageTool,
24
+ registerListStyleguidesTool,
25
+ registerGetStyleguideTreeTool,
26
+ ].forEach((fn) => fn(server));
27
+ // Register all resources
28
+ [
29
+ registerStyleguideListResource,
30
+ registerStyleguideResource,
31
+ registerListPagesResource,
32
+ registerPageResource,
33
+ ].forEach((fn) => fn(server));
34
+ // Register all prompts
35
+ [registerRecommendPagesPrompt, recommendSummarizePagePrompt].forEach((fn) => fn(server));
36
+ return server;
37
+ }
38
+ // Start server on STDIO
39
+ async function main() {
40
+ const server = createServer();
41
+ const transport = new StdioServerTransport();
42
+ await server.connect(transport);
43
+ console.error("zeroheight MCP Server running on stdio");
44
+ }
45
+ main().catch((error) => {
46
+ console.error("Fatal error in main():", error);
47
+ process.exit(1);
48
+ });
@@ -0,0 +1,28 @@
1
+ import z from "zod";
2
+ export function recommendSummarizePagePrompt(server) {
3
+ server.registerPrompt("summarize-page", {
4
+ title: "Summarize Page",
5
+ description: "Give a TLDR page summary",
6
+ argsSchema: {
7
+ pageId: z.string(),
8
+ releaseId: z.string().optional(),
9
+ },
10
+ }, ({ pageId, releaseId }) => ({
11
+ messages: [
12
+ {
13
+ role: "assistant",
14
+ content: {
15
+ type: "text",
16
+ text: "Your goal is to find the page in zeroheight and summarize it.",
17
+ },
18
+ },
19
+ {
20
+ role: "user",
21
+ content: {
22
+ type: "text",
23
+ text: `Summarize the following page: ${pageId}.${releaseId ? `Get the page from this release: ${releaseId}` : ""}`,
24
+ },
25
+ },
26
+ ],
27
+ }));
28
+ }
@@ -0,0 +1,28 @@
1
+ import z from "zod";
2
+ export function registerRecommendPagesPrompt(server) {
3
+ server.registerPrompt("recommend-pages", {
4
+ title: "Recommend Pages",
5
+ description: "Find useful pages on a given topic",
6
+ argsSchema: {
7
+ styleguideId: z.string(),
8
+ topic: z.string(),
9
+ },
10
+ }, ({ styleguideId, topic }) => ({
11
+ messages: [
12
+ {
13
+ role: "assistant",
14
+ content: {
15
+ type: "text",
16
+ text: "Your goal is to find pages in a styleguide and recommend the top 5 most relevant to the given task.",
17
+ },
18
+ },
19
+ {
20
+ role: "user",
21
+ content: {
22
+ type: "text",
23
+ text: `Look at the navigation tree for this styleguide: ${styleguideId}.\n\nReturn the URLs for the top 5 most relevant pages about this topic: ${topic}.`,
24
+ },
25
+ },
26
+ ],
27
+ }));
28
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Page Resources
3
+ */
4
+ import { ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { completionForStyleduideId, listStyleguideResources, } from "./styleguide.js";
6
+ import { getPage, listPages } from "../api/page.js";
7
+ import { formatPageAsMarkdown } from "../common/formatters/page.js";
8
+ export function registerListPagesResource(server) {
9
+ server.registerResource("styleguide-page-list", new ResourceTemplate("zeroheight://styleguide/{styleguideId}/page-list", {
10
+ list: listStyleguideResources,
11
+ complete: {
12
+ styleguideId: completionForStyleduideId,
13
+ },
14
+ }), {
15
+ title: "List the pages for a styleguide",
16
+ description: "List all pages in a styleguide",
17
+ mimeType: "text/markdown",
18
+ }, async (uri, { styleguideId }) => {
19
+ const selectedId = Array.isArray(styleguideId)
20
+ ? styleguideId[0]
21
+ : styleguideId;
22
+ const parsedStyleguideId = parseInt(selectedId, 10);
23
+ const pages = await listPages(parsedStyleguideId);
24
+ return {
25
+ contents: [
26
+ {
27
+ uri: uri.href,
28
+ text: pages
29
+ .map((page) => `- ${page.name} (id: ${page.id})`)
30
+ .join("\n"),
31
+ },
32
+ ],
33
+ };
34
+ });
35
+ }
36
+ export function registerPageResource(server) {
37
+ server.registerResource("page", new ResourceTemplate("zeroheight://styleguide/{styleguideId}/page/{pageId}", {
38
+ list: undefined,
39
+ complete: {
40
+ styleguideId: completionForStyleduideId,
41
+ pageId: completionForStyleduideId,
42
+ },
43
+ }), {
44
+ title: "Page",
45
+ description: "Design system page",
46
+ mimeType: "text/markdown",
47
+ }, async (uri, { pageId }) => {
48
+ const selectedId = Array.isArray(pageId) ? pageId[0] : pageId;
49
+ const parsedPageId = parseInt(selectedId, 10);
50
+ const page = await getPage(parsedPageId);
51
+ if (!page) {
52
+ throw new Error("Couldn't find the page");
53
+ }
54
+ return {
55
+ contents: [
56
+ {
57
+ uri: uri.href,
58
+ text: formatPageAsMarkdown(page),
59
+ mimeType: "text/markdown",
60
+ },
61
+ ],
62
+ };
63
+ });
64
+ }
65
+ /** Utils */
66
+ export async function completionForPageId(searchTerm, context) {
67
+ const styleguideId = parseInt(context.arguments?.styleguideId ?? "", 10);
68
+ const pageList = await listPages(styleguideId);
69
+ const matchingPages = pageList.filter((page) => page.id
70
+ .toString()
71
+ .toLocaleLowerCase()
72
+ .includes(searchTerm.toLocaleLowerCase()));
73
+ return matchingPages.map((page) => page.id.toString());
74
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Styleguide Resources
3
+ */
4
+ import { ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { getStyleguideTree, listStyleguides } from "../api/styleguide.js";
6
+ import { DEFAULT_NAME, formatTreeAsJson, } from "../common/formatters/styleguide.js";
7
+ export function registerStyleguideListResource(server) {
8
+ server.registerResource("styleguide-list", "zeroheight://styleguide/list", {
9
+ title: "Styleguide list",
10
+ description: "List all accessible styleguides",
11
+ mimeType: "application/json",
12
+ }, async (uri) => {
13
+ const styleguideList = await listStyleguides();
14
+ const styleguideResources = styleguideList.map((styleguide) => ({
15
+ id: styleguide.id,
16
+ name: styleguide.name ?? DEFAULT_NAME,
17
+ }));
18
+ return {
19
+ contents: [
20
+ {
21
+ uri: uri.href,
22
+ text: JSON.stringify(styleguideResources, null, 2),
23
+ mimeType: "application/json",
24
+ },
25
+ ],
26
+ };
27
+ });
28
+ }
29
+ export function registerStyleguideResource(server) {
30
+ server.registerResource("styleguide", new ResourceTemplate("zeroheight://styleguide/{styleguideId}", {
31
+ list: undefined,
32
+ complete: {
33
+ styleguideId: completionForStyleduideId,
34
+ },
35
+ }), {
36
+ title: "Styleguide Navigation",
37
+ description: "Access the styleguide's navigation including categories, pages and tabs",
38
+ }, async (uri, { styleguideId }) => {
39
+ console.log({ uri, styleguideId });
40
+ const selectedId = Array.isArray(styleguideId)
41
+ ? styleguideId[0]
42
+ : styleguideId;
43
+ console.log({ selectedId });
44
+ const parsedStyleguideId = parseInt(selectedId, 10);
45
+ // TODO: Make endpoint to get basic styleguide info instead
46
+ const styleguideList = await listStyleguides();
47
+ console.log({ styleguideList });
48
+ const currentStyleguide = styleguideList.find((styleguide) => styleguide.id === parsedStyleguideId);
49
+ if (!currentStyleguide) {
50
+ return { contents: [] };
51
+ }
52
+ const tree = await getStyleguideTree(parsedStyleguideId);
53
+ const formattedTree = formatTreeAsJson(tree);
54
+ return {
55
+ contents: [
56
+ {
57
+ mimeType: "application/json",
58
+ uri: uri.href,
59
+ text: JSON.stringify(formattedTree, null, 2),
60
+ },
61
+ ],
62
+ };
63
+ });
64
+ }
65
+ /** Utils */
66
+ export async function completionForStyleduideId(searchTerm) {
67
+ const styleguideList = await listStyleguides();
68
+ const matchingStyleguides = styleguideList.filter((styleguide) => styleguide.id
69
+ .toString()
70
+ .toLocaleLowerCase()
71
+ .includes(searchTerm.toLocaleLowerCase()));
72
+ return matchingStyleguides.map((styleguide) => styleguide.id.toString());
73
+ }
74
+ export async function listStyleguideResources() {
75
+ const styleguideList = await listStyleguides();
76
+ const resources = styleguideList.map((styleguide) => ({
77
+ uri: `zeroheight://styleguide/${styleguide.id}`,
78
+ name: `${styleguide.name ?? DEFAULT_NAME}`,
79
+ _meta: {
80
+ id: styleguide.id,
81
+ shareId: styleguide.share_id,
82
+ },
83
+ }));
84
+ return {
85
+ resources,
86
+ };
87
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Page Tools
3
+ */
4
+ import z from "zod";
5
+ import { getPage, listPages } from "../api/page.js";
6
+ import { formatPageAsMarkdown, formatPageListItemsAsMarkdown, } from "../common/formatters/page.js";
7
+ export function registerListPagesTool(server) {
8
+ server.registerTool("list-pages", {
9
+ title: "List pages for a styleguide",
10
+ description: "List all accessible pages in a styleguide",
11
+ inputSchema: {
12
+ styleguideId: z.number().int().positive(),
13
+ releaseId: z.number().int().optional(),
14
+ searchTerm: z.string().optional(),
15
+ responseFormat: z
16
+ .enum(["markdown", "resource_links"])
17
+ .default("markdown"),
18
+ },
19
+ }, async ({ styleguideId, releaseId, searchTerm, responseFormat }) => {
20
+ const pages = await listPages(styleguideId, { releaseId, searchTerm });
21
+ if (pages.length === 0) {
22
+ return {
23
+ isError: true,
24
+ content: [
25
+ {
26
+ type: "text",
27
+ text: "There are no pages in this styleguide, check you have the correct styleguide ID",
28
+ },
29
+ ],
30
+ };
31
+ }
32
+ if (responseFormat === "markdown") {
33
+ return {
34
+ content: [
35
+ {
36
+ type: "text",
37
+ text: formatPageListItemsAsMarkdown(pages),
38
+ mimeType: "text/markdown",
39
+ },
40
+ ],
41
+ };
42
+ }
43
+ const resourceLinks = pages.map((page) => ({
44
+ type: "resource_link",
45
+ uri: `zeroheight://styleguide/${styleguideId}/page/${page.id}`,
46
+ name: page.name ?? "Unnamed Page",
47
+ _meta: {
48
+ createdAt: page.created_at,
49
+ id: page.id,
50
+ },
51
+ mimeType: "text/markdown",
52
+ description: "Styleguide page",
53
+ }));
54
+ return {
55
+ content: [
56
+ {
57
+ type: "text",
58
+ text: `There are ${pages.length} pages in the styleguide: `,
59
+ },
60
+ ...resourceLinks,
61
+ ],
62
+ };
63
+ });
64
+ }
65
+ export function registerGetPageTool(server) {
66
+ server.registerTool("get-page", {
67
+ title: "Get a styleguide page",
68
+ description: "Get a page in the requested format",
69
+ inputSchema: {
70
+ pageId: z.string(),
71
+ releaseId: z.number().int().optional(),
72
+ responseFormat: z.enum(["markdown", "json"]).default("json"),
73
+ },
74
+ }, async ({ pageId, responseFormat }) => {
75
+ const page = await getPage(pageId);
76
+ if (!page) {
77
+ return {
78
+ isError: true,
79
+ content: [
80
+ {
81
+ type: "text",
82
+ text: "Could not find the page, check you have the correct page ID and release ID.",
83
+ },
84
+ ],
85
+ };
86
+ }
87
+ if (responseFormat === "markdown") {
88
+ return {
89
+ content: [
90
+ {
91
+ type: "text",
92
+ text: formatPageAsMarkdown(page),
93
+ mimeType: "text/markdown",
94
+ },
95
+ ],
96
+ };
97
+ }
98
+ return {
99
+ content: [
100
+ {
101
+ type: "text",
102
+ text: JSON.stringify(page, null, 2),
103
+ mimeType: "application/json",
104
+ },
105
+ ],
106
+ };
107
+ });
108
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Styleguide Tools
3
+ */
4
+ import z from "zod";
5
+ import { getStyleguideTree, listStyleguides } from "../api/styleguide.js";
6
+ import { DEFAULT_NAME, formatStyleguideListAsMarkdown, formatTreeAsJson, } from "../common/formatters/styleguide.js";
7
+ export function registerListStyleguidesTool(server) {
8
+ server.registerTool("list-styleguides", {
9
+ title: "List styleguides",
10
+ description: "List all accessible styleguides as resource links",
11
+ inputSchema: {
12
+ responseType: z
13
+ .enum(["markdown", "resource_links"])
14
+ .default("markdown"),
15
+ },
16
+ }, async ({ responseType }) => {
17
+ const styleguides = await listStyleguides();
18
+ if (styleguides.length === 0) {
19
+ return {
20
+ isError: true,
21
+ content: [
22
+ {
23
+ type: "text",
24
+ text: "There are no accessible styleguides. Check your auth credentials and make sure you have at least one styleguide in zeroheight.",
25
+ },
26
+ ],
27
+ };
28
+ }
29
+ if (responseType === "markdown") {
30
+ return {
31
+ content: [
32
+ {
33
+ type: "text",
34
+ text: formatStyleguideListAsMarkdown(styleguides),
35
+ mimeType: "text/markdown",
36
+ },
37
+ ],
38
+ };
39
+ }
40
+ const resourceLinks = styleguides.map((styleguide) => ({
41
+ type: "resource_link",
42
+ uri: `zeroheight://styleguide/${styleguide.id}`,
43
+ name: styleguide.name ?? DEFAULT_NAME,
44
+ _meta: {
45
+ shareId: styleguide.share_id,
46
+ id: styleguide.id,
47
+ },
48
+ mimeType: "application/json",
49
+ description: "Navigation structure for the styleguide",
50
+ }));
51
+ return {
52
+ content: [
53
+ {
54
+ type: "text",
55
+ text: `There are available ${styleguides.length} styleguides`,
56
+ },
57
+ ...resourceLinks,
58
+ ],
59
+ };
60
+ });
61
+ }
62
+ export function registerGetStyleguideTreeTool(server) {
63
+ server.registerTool("get-styleguide-tree", {
64
+ title: "Get styleguide tree",
65
+ description: "A navigation hierarchy for a styleguide with top-level navigation, categories, pages and tabs",
66
+ inputSchema: {
67
+ styleguideId: z.number().int().positive(),
68
+ },
69
+ }, async ({ styleguideId }) => {
70
+ try {
71
+ const tree = await getStyleguideTree(styleguideId);
72
+ const formattedTree = formatTreeAsJson(tree);
73
+ return {
74
+ content: [
75
+ {
76
+ type: "text",
77
+ text: JSON.stringify(formattedTree, null, 2),
78
+ },
79
+ ],
80
+ };
81
+ }
82
+ catch {
83
+ return {
84
+ isError: true,
85
+ content: [
86
+ {
87
+ type: "text",
88
+ text: "Could not get the navigation tree for this styleguide.",
89
+ },
90
+ ],
91
+ };
92
+ }
93
+ });
94
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@zeroheight/mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for zeroheight",
5
+ "type": "module",
6
+ "bin": {
7
+ "mcp-server": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc && chmod 755 dist/index.js",
11
+ "start": "node dist/index.js",
12
+ "cleanup": "rm -rf dist",
13
+ "lint": "prettier --write .",
14
+ "test": "rstest"
15
+ },
16
+ "engines": {
17
+ "node": ">= 22"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "CHANGELOG.md"
22
+ ],
23
+ "keywords": [],
24
+ "author": "zeroheight",
25
+ "license": "ISC",
26
+ "bugs": {
27
+ "email": "support@zeroheight.com"
28
+ },
29
+ "homepage": "https://zeroheight.com",
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.17.1",
32
+ "zod": "^3.25.76"
33
+ },
34
+ "devDependencies": {
35
+ "@rstest/core": "^0.1.2",
36
+ "@types/node": "^24.2.0",
37
+ "prettier": "^3.6.2",
38
+ "typescript": "^5.9.2"
39
+ }
40
+ }