@primer/mcp 0.4.0 → 0.5.0-rc.01176969a

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/dist/stdio.js CHANGED
@@ -1,23 +1,7 @@
1
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
2
- import { s as server } from './server-CkSoZOVD.js';
3
- import '@modelcontextprotocol/sdk/server/mcp.js';
4
- import 'cheerio';
5
- import 'zod';
6
- import 'turndown';
7
- import '@primer/react/generated/components.json' with { type: 'json' };
8
- import '@primer/octicons/build/data.json' with { type: 'json' };
9
- import 'node:fs';
10
- import 'node:module';
11
- import 'child_process';
12
- import '@primer/primitives/dist/docs/base/motion/motion.json' with { type: 'json' };
13
- import '@primer/primitives/dist/docs/base/size/size.json' with { type: 'json' };
14
- import '@primer/primitives/dist/docs/base/typography/typography.json' with { type: 'json' };
15
- import '@primer/primitives/dist/docs/functional/size/border.json' with { type: 'json' };
16
- import '@primer/primitives/dist/docs/functional/size/size-coarse.json' with { type: 'json' };
17
- import '@primer/primitives/dist/docs/functional/size/size-fine.json' with { type: 'json' };
18
- import '@primer/primitives/dist/docs/functional/size/size.json' with { type: 'json' };
19
- import '@primer/primitives/dist/docs/functional/themes/light.json' with { type: 'json' };
20
- import '@primer/primitives/dist/docs/functional/typography/typography.json' with { type: 'json' };
21
-
1
+ import { t as server } from "./server-rl4CHqO6.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ //#region src/transports/stdio.ts
22
4
  const transport = new StdioServerTransport();
23
5
  await server.connect(transport);
6
+ //#endregion
7
+ export {};
@@ -0,0 +1 @@
1
+ export { };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@primer/mcp",
3
3
  "description": "An MCP server that connects AI tools to the Primer Design System",
4
- "version": "0.4.0",
4
+ "version": "0.5.0-rc.01176969a",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "mcp": "./bin/mcp.js"
@@ -28,34 +28,27 @@
28
28
  },
29
29
  "scripts": {
30
30
  "clean": "rimraf dist",
31
- "build": "rollup -c",
31
+ "build": "rolldown -c",
32
+ "test": "vitest --run",
32
33
  "type-check": "tsc --noEmit",
33
- "watch": "rollup -c -w"
34
+ "watch": "rolldown -c -w"
34
35
  },
35
36
  "dependencies": {
36
- "@babel/runtime": "^7.28.6",
37
37
  "@modelcontextprotocol/sdk": "^1.24.0",
38
38
  "@primer/octicons": "^19.15.5",
39
39
  "@primer/primitives": "10.x || 11.x",
40
- "@primer/react": "^38.30.0",
40
+ "@primer/react": "^38.33.0",
41
41
  "cheerio": "^1.0.0",
42
42
  "turndown": "^7.2.0",
43
43
  "zod": "^4.3.5"
44
44
  },
45
45
  "devDependencies": {
46
- "@babel/core": "^7.29.0",
47
- "@babel/plugin-transform-runtime": "^7.29.0",
48
- "@babel/preset-env": "^7.29.0",
49
- "@babel/preset-typescript": "^7.28.5",
50
46
  "@modelcontextprotocol/inspector": "^0.16.6",
51
- "@rollup/plugin-babel": "^6.1.0",
52
- "@rollup/plugin-commonjs": "^29.0.0",
53
- "@rollup/plugin-json": "^6.1.0",
54
- "@rollup/plugin-node-resolve": "^16.0.3",
47
+ "@primer/vitest-config": "^0.0.0",
55
48
  "@types/turndown": "^5.0.5",
56
49
  "rimraf": "^6.0.1",
57
- "rollup": "^4.59.0",
58
- "rollup-plugin-typescript2": "^0.36.0",
50
+ "rolldown": "^1.1.4",
51
+ "rolldown-plugin-dts": "^0.26.0",
59
52
  "typescript": "^6.0.3"
60
53
  }
61
54
  }
@@ -0,0 +1,125 @@
1
+ import {Client} from '@modelcontextprotocol/sdk/client/index.js'
2
+ import {InMemoryTransport} from '@modelcontextprotocol/sdk/inMemory.js'
3
+ import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest'
4
+ import {server} from './server'
5
+
6
+ describe('get_component_batch', () => {
7
+ const client = new Client({name: 'mcp-server-test', version: '1.0.0'})
8
+
9
+ beforeAll(async () => {
10
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
11
+ await server.connect(serverTransport)
12
+ await client.connect(clientTransport)
13
+ })
14
+
15
+ beforeEach(() => {
16
+ vi.stubGlobal('fetch', vi.fn<typeof fetch>())
17
+ })
18
+
19
+ afterEach(() => {
20
+ vi.useRealTimers()
21
+ vi.unstubAllGlobals()
22
+ })
23
+
24
+ afterAll(async () => {
25
+ await client.close()
26
+ await server.close()
27
+ })
28
+
29
+ const callBatch = (names: string[]) => {
30
+ return client.callTool({
31
+ name: 'get_component_batch',
32
+ arguments: {names},
33
+ })
34
+ }
35
+
36
+ it('requires between 2 and 10 names', async () => {
37
+ const tooFew = await callBatch(['Button'])
38
+ const tooMany = await callBatch(Array.from({length: 11}, (_, index) => `Component${index}`))
39
+
40
+ expect(tooFew.isError).toBe(true)
41
+ expect(tooMany.isError).toBe(true)
42
+ expect(fetch).not.toHaveBeenCalled()
43
+ })
44
+
45
+ it('deduplicates names case-insensitively and preserves result order', async () => {
46
+ const fetchMock = vi.mocked(fetch)
47
+ fetchMock.mockImplementation(async input => {
48
+ const url = new URL(input.toString())
49
+ return new Response(`${url.pathname} docs`)
50
+ })
51
+
52
+ const result = await callBatch(['Button', 'button', 'Dialog'])
53
+
54
+ expect(fetchMock).toHaveBeenCalledTimes(2)
55
+ expect(result.content).toEqual([
56
+ {type: 'text', text: '/product/components/button/llms.txt docs'},
57
+ {type: 'text', text: '/product/components/dialog/llms.txt docs'},
58
+ ])
59
+ })
60
+
61
+ it('starts component fetches in parallel', async () => {
62
+ const fetchMock = vi.mocked(fetch)
63
+ const pendingResponses: Array<(response: Response) => void> = []
64
+ fetchMock.mockImplementation(
65
+ () =>
66
+ new Promise(resolve => {
67
+ pendingResponses.push(resolve)
68
+ }),
69
+ )
70
+
71
+ const resultPromise = callBatch(['Button', 'Dialog'])
72
+ await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(2))
73
+
74
+ for (const resolve of pendingResponses) {
75
+ resolve(new Response('docs'))
76
+ }
77
+
78
+ await expect(resultPromise).resolves.toMatchObject({
79
+ content: [
80
+ {type: 'text', text: 'docs'},
81
+ {type: 'text', text: 'docs'},
82
+ ],
83
+ })
84
+ })
85
+
86
+ it('returns missing components and fetch failures in-band', async () => {
87
+ vi.mocked(fetch).mockResolvedValue(new Response(null, {status: 500}))
88
+
89
+ const result = await callBatch(['MissingComponent', 'button'])
90
+
91
+ expect(result.isError).not.toBe(true)
92
+ expect(result.content).toEqual([
93
+ {
94
+ type: 'text',
95
+ text: '## MissingComponent\n\nStatus: not-found. No component named `MissingComponent` exists in @primer/react. Use `list_components` for valid names.',
96
+ },
97
+ {
98
+ type: 'text',
99
+ text: '## Button\n\nStatus: fetch-error. Failed to load documentation for Button.',
100
+ },
101
+ ])
102
+ })
103
+
104
+ it('times out stalled requests without leaving timers active', async () => {
105
+ vi.useFakeTimers()
106
+ const fetchMock = vi.mocked(fetch)
107
+ fetchMock.mockImplementation(
108
+ (_input, init) =>
109
+ new Promise((_resolve, reject) => {
110
+ init?.signal?.addEventListener('abort', () => reject(init.signal?.reason), {once: true})
111
+ }),
112
+ )
113
+
114
+ const resultPromise = callBatch(['Button', 'Dialog'])
115
+ await vi.advanceTimersByTimeAsync(10_000)
116
+
117
+ await expect(resultPromise).resolves.toMatchObject({
118
+ content: [
119
+ {type: 'text', text: '## Button\n\nStatus: fetch-error. Failed to load documentation for Button.'},
120
+ {type: 'text', text: '## Dialog\n\nStatus: fetch-error. Failed to load documentation for Dialog.'},
121
+ ],
122
+ })
123
+ expect(vi.getTimerCount()).toBe(0)
124
+ })
125
+ })
package/src/server.ts CHANGED
@@ -161,6 +161,73 @@ server.registerTool(
161
161
  },
162
162
  )
163
163
 
164
+ server.registerTool(
165
+ 'get_component_batch',
166
+ {
167
+ description:
168
+ 'Retrieve documentation for multiple Primer React components in one call. ' +
169
+ 'Pass all component names you need at once. Docs are fetched in parallel to avoid repeated MCP round-trips when resolving several components. ' +
170
+ 'For a single component, prefer get_component instead.',
171
+ inputSchema: {
172
+ names: z
173
+ .array(z.string())
174
+ .min(2)
175
+ .max(10)
176
+ .describe('Component names to retrieve (e.g. ["ActionMenu", "Button", "Pagination"])'),
177
+ },
178
+ annotations: {readOnlyHint: true},
179
+ },
180
+ async ({names}) => {
181
+ const seenNames = new Set<string>()
182
+ const deduped = names.filter(name => {
183
+ const normalizedName = name.toLowerCase()
184
+ if (seenNames.has(normalizedName)) {
185
+ return false
186
+ }
187
+ seenNames.add(normalizedName)
188
+ return true
189
+ })
190
+ const components = listComponents()
191
+
192
+ const results = await Promise.all(
193
+ deduped.map(async name => {
194
+ const match = components.find(
195
+ component => component.name === name || component.name.toLowerCase() === name.toLowerCase(),
196
+ )
197
+ if (!match) {
198
+ return {
199
+ type: 'text' as const,
200
+ text: `## ${name}\n\nStatus: not-found. No component named \`${name}\` exists in @primer/react. Use \`list_components\` for valid names.`,
201
+ }
202
+ }
203
+
204
+ const controller = new AbortController()
205
+ const timeout = setTimeout(() => controller.abort(), 10_000)
206
+
207
+ try {
208
+ const llmsUrl = new URL(`/product/components/${match.slug}/llms.txt`, 'https://primer.style')
209
+ const llmsResponse = await fetch(llmsUrl, {signal: controller.signal})
210
+ if (llmsResponse.ok) {
211
+ const text = await llmsResponse.text()
212
+ return {type: 'text' as const, text}
213
+ }
214
+ } catch (_: unknown) {
215
+ // Fall through to the in-band error result so other component requests can succeed.
216
+ } finally {
217
+ clearTimeout(timeout)
218
+ }
219
+
220
+ return {
221
+ type: 'text' as const,
222
+ text: `## ${match.name}\n\nStatus: fetch-error. Failed to load documentation for ${match.name}.`,
223
+ }
224
+ }),
225
+ )
226
+
227
+ return {content: results}
228
+ },
229
+ )
230
+
164
231
  server.registerTool(
165
232
  'get_component_examples',
166
233
  {