@primer/mcp 0.0.4 → 0.2.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/src/server.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'
2
2
  // eslint-disable-next-line import/no-namespace
3
3
  import * as cheerio from 'cheerio'
4
- import {z} from 'zod'
4
+ // eslint-disable-next-line import/no-namespace
5
+ import * as z from 'zod'
5
6
  import TurndownService from 'turndown'
6
7
  import {listComponents, listPatterns, listIcons} from './primer'
7
8
  import {tokens, serialize} from './primitives'
@@ -17,35 +18,40 @@ const turndownService = new TurndownService()
17
18
  // -----------------------------------------------------------------------------
18
19
  // Project setup
19
20
  // -----------------------------------------------------------------------------
20
- server.tool('init', 'Setup or create a project that includes Primer React', async () => {
21
- const url = new URL(`/product/getting-started/react`, 'https://primer.style')
22
- const response = await fetch(url)
23
- if (!response.ok) {
24
- throw new Error(`Failed to fetch ${url}: ${response.statusText}`)
25
- }
21
+ server.registerTool(
22
+ 'init',
23
+ {
24
+ description: 'Setup or create a project that includes Primer React',
25
+ },
26
+ async () => {
27
+ const url = new URL(`/product/getting-started/react`, 'https://primer.style')
28
+ const response = await fetch(url)
29
+ if (!response.ok) {
30
+ throw new Error(`Failed to fetch ${url}: ${response.statusText}`)
31
+ }
26
32
 
27
- const html = await response.text()
28
- if (!html) {
29
- return {
30
- content: [],
33
+ const html = await response.text()
34
+ if (!html) {
35
+ return {
36
+ content: [],
37
+ }
31
38
  }
32
- }
33
39
 
34
- const $ = cheerio.load(html)
35
- const source = $('main').html()
36
- if (!source) {
37
- return {
38
- content: [],
40
+ const $ = cheerio.load(html)
41
+ const source = $('main').html()
42
+ if (!source) {
43
+ return {
44
+ content: [],
45
+ }
39
46
  }
40
- }
41
47
 
42
- const text = turndownService.turndown(source)
48
+ const text = turndownService.turndown(source)
43
49
 
44
- return {
45
- content: [
46
- {
47
- type: 'text',
48
- text: `The getting started documentation for Primer React is included below. It's important that the project:
50
+ return {
51
+ content: [
52
+ {
53
+ type: 'text',
54
+ text: `The getting started documentation for Primer React is included below. It's important that the project:
49
55
 
50
56
  - Is using a tool like Vite, Next.js, etc that supports TypeScript and React. If the project does not have support for that, generate an appropriate project scaffold
51
57
  - Installs the latest version of \`@primer/react\` from \`npm\`
@@ -58,37 +64,45 @@ server.tool('init', 'Setup or create a project that includes Primer React', asyn
58
64
 
59
65
  ${text}
60
66
  `,
61
- },
62
- ],
63
- }
64
- })
67
+ },
68
+ ],
69
+ }
70
+ },
71
+ )
65
72
 
66
73
  // -----------------------------------------------------------------------------
67
74
  // Components
68
75
  // -----------------------------------------------------------------------------
69
- server.tool('list_components', 'List all of the components available from Primer React', async () => {
70
- const components = listComponents().map(component => {
71
- return `- ${component.name}`
72
- })
73
- return {
74
- content: [
75
- {
76
- type: 'text',
77
- text: `The following components are available in the @primer/react in TypeScript projects:
76
+ server.registerTool(
77
+ 'list_components',
78
+ {description: 'List all of the components available from Primer React'},
79
+ async () => {
80
+ const components = listComponents().map(component => {
81
+ return `- ${component.name}`
82
+ })
83
+ return {
84
+ content: [
85
+ {
86
+ type: 'text',
87
+ text: `The following components are available in the @primer/react in TypeScript projects:
78
88
 
79
89
  ${components.join('\n')}
80
90
 
81
91
  You can use the \`get_component\` tool to get more information about a specific component. You can use these components from the @primer/react package.`,
82
- },
83
- ],
84
- }
85
- })
92
+ },
93
+ ],
94
+ }
95
+ },
96
+ )
86
97
 
87
- server.tool(
98
+ server.registerTool(
88
99
  'get_component',
89
- 'Get a specific component by name',
90
100
  {
91
- name: z.string().describe('The name of the component to retrieve'),
101
+ description:
102
+ 'Retrieve documentation and usage details for a specific React component from the @primer/react package by its name. This tool provides the official Primer documentation for any listed component, making it easy to inspect, reuse, or integrate components in your project.',
103
+ inputSchema: {
104
+ name: z.string().describe('The name of the component to retrieve'),
105
+ },
92
106
  },
93
107
  async ({name}) => {
94
108
  const components = listComponents()
@@ -97,12 +111,27 @@ server.tool(
97
111
  })
98
112
  if (!match) {
99
113
  return {
100
- content: [
101
- {
102
- type: 'text',
103
- text: `There is no component named \`${name}\` in the @primer/react package. For a full list of components, use the \`get_components\` tool.`,
104
- },
105
- ],
114
+ isError: true,
115
+ errorMessage: `There is no component named \`${name}\` in the @primer/react package. For a full list of components, use the \`list_components\` tool.`,
116
+ content: [],
117
+ }
118
+ }
119
+
120
+ const llmsUrl = new URL(`/product/components/${match.slug}/llms.txt`, 'https://primer.style')
121
+ const llmsResponse = await fetch(llmsUrl)
122
+ if (llmsResponse.ok) {
123
+ try {
124
+ const llmsText = await llmsResponse.text()
125
+ return {
126
+ content: [
127
+ {
128
+ type: 'text',
129
+ text: llmsText,
130
+ },
131
+ ],
132
+ }
133
+ } catch (_: unknown) {
134
+ // If there's an error fetching or processing the llms.txt, we fall back to the regular documentation
106
135
  }
107
136
  }
108
137
 
@@ -141,11 +170,13 @@ ${text}`,
141
170
  },
142
171
  )
143
172
 
144
- server.tool(
173
+ server.registerTool(
145
174
  'get_component_examples',
146
- 'Get examples for how to use a component from Primer React',
147
175
  {
148
- name: z.string().describe('The name of the component to retrieve'),
176
+ description: 'Get examples for how to use a component from Primer React',
177
+ inputSchema: {
178
+ name: z.string().describe('The name of the component to retrieve'),
179
+ },
149
180
  },
150
181
  async ({name}) => {
151
182
  const components = listComponents()
@@ -199,11 +230,13 @@ ${text}`,
199
230
  },
200
231
  )
201
232
 
202
- server.tool(
233
+ server.registerTool(
203
234
  'get_component_usage_guidelines',
204
- 'Get usage information for how to use a component from Primer',
205
235
  {
206
- name: z.string().describe('The name of the component to retrieve'),
236
+ description: 'Get usage information for how to use a component from Primer',
237
+ inputSchema: {
238
+ name: z.string().describe('The name of the component to retrieve'),
239
+ },
207
240
  },
208
241
  async ({name}) => {
209
242
  const components = listComponents()
@@ -268,11 +301,14 @@ ${text}`,
268
301
  },
269
302
  )
270
303
 
271
- server.tool(
304
+ server.registerTool(
272
305
  'get_component_accessibility_guidelines',
273
- 'Get accessibility information for how to use a component from Primer React',
274
306
  {
275
- name: z.string().describe('The name of the component to retrieve'),
307
+ description:
308
+ 'Retrieve accessibility guidelines and best practices for a specific component from the @primer/react package by its name. Use this tool to get official accessibility recommendations, usage tips, and requirements to ensure your UI components are inclusive and meet accessibility standards.',
309
+ inputSchema: {
310
+ name: z.string().describe('The name of the component to retrieve'),
311
+ },
276
312
  },
277
313
  async ({name}) => {
278
314
  const components = listComponents()
@@ -284,7 +320,7 @@ server.tool(
284
320
  content: [
285
321
  {
286
322
  type: 'text',
287
- text: `There is no component named \`${name}\` in the @primer/react package. For a full list of components, use the \`get_components\` tool.`,
323
+ text: `There is no component named \`${name}\` in the @primer/react package. For a full list of components, use the \`list_components\` tool.`,
288
324
  },
289
325
  ],
290
326
  }
@@ -340,27 +376,33 @@ ${text}`,
340
376
  // -----------------------------------------------------------------------------
341
377
  // Patterns
342
378
  // -----------------------------------------------------------------------------
343
- server.tool('list_patterns', 'List all of the patterns available from Primer React', async () => {
344
- const patterns = listPatterns().map(pattern => {
345
- return `- ${pattern.name}`
346
- })
347
- return {
348
- content: [
349
- {
350
- type: 'text',
351
- text: `The following patterns are available in the @primer/react in TypeScript projects:
379
+ server.registerTool(
380
+ 'list_patterns',
381
+ {description: 'List all of the patterns available from Primer React'},
382
+ async () => {
383
+ const patterns = listPatterns().map(pattern => {
384
+ return `- ${pattern.name}`
385
+ })
386
+ return {
387
+ content: [
388
+ {
389
+ type: 'text',
390
+ text: `The following patterns are available in the @primer/react in TypeScript projects:
352
391
 
353
392
  ${patterns.join('\n')}`,
354
- },
355
- ],
356
- }
357
- })
393
+ },
394
+ ],
395
+ }
396
+ },
397
+ )
358
398
 
359
- server.tool(
399
+ server.registerTool(
360
400
  'get_pattern',
361
- 'Get a specific pattern by name',
362
401
  {
363
- name: z.string().describe('The name of the pattern to retrieve'),
402
+ description: 'Get a specific pattern by name',
403
+ inputSchema: {
404
+ name: z.string().describe('The name of the pattern to retrieve'),
405
+ },
364
406
  },
365
407
  async ({name}) => {
366
408
  const patterns = listPatterns()
@@ -417,7 +459,7 @@ ${text}`,
417
459
  // -----------------------------------------------------------------------------
418
460
  // Design Tokens
419
461
  // -----------------------------------------------------------------------------
420
- server.tool('list_tokens', 'List all of the design tokens available from Primer', async () => {
462
+ server.registerTool('list_tokens', {description: 'List all of the design tokens available from Primer'}, async () => {
421
463
  let text =
422
464
  'Below is a list of all design tokens available from Primer. Tokens are used in CSS and CSS Modules. To refer to the CSS Custom Property for a design token, wrap it in var(--{name-of-token}). To learn how to use a specific token, use a corresponding usage tool for the category of the token. For example, if a token is a color token look for the get_color_usage tool. \n\n'
423
465
 
@@ -436,108 +478,122 @@ server.tool('list_tokens', 'List all of the design tokens available from Primer'
436
478
  // -----------------------------------------------------------------------------
437
479
  // Foundations
438
480
  // -----------------------------------------------------------------------------
439
- server.tool('get_color_usage', 'Get the guidelines for how to apply color to a user interface', async () => {
440
- const url = new URL(`/product/getting-started/foundations/color-usage`, 'https://primer.style')
441
- const response = await fetch(url)
442
- if (!response.ok) {
443
- throw new Error(`Failed to fetch ${url} - ${response.statusText}`)
444
- }
481
+ server.registerTool(
482
+ 'get_color_usage',
483
+ {description: 'Get the guidelines for how to apply color to a user interface'},
484
+ async () => {
485
+ const url = new URL(`/product/getting-started/foundations/color-usage`, 'https://primer.style')
486
+ const response = await fetch(url)
487
+ if (!response.ok) {
488
+ throw new Error(`Failed to fetch ${url} - ${response.statusText}`)
489
+ }
445
490
 
446
- const html = await response.text()
447
- if (!html) {
448
- return {
449
- content: [],
491
+ const html = await response.text()
492
+ if (!html) {
493
+ return {
494
+ content: [],
495
+ }
450
496
  }
451
- }
452
497
 
453
- const $ = cheerio.load(html)
454
- const source = $('main').html()
455
- if (!source) {
456
- return {
457
- content: [],
498
+ const $ = cheerio.load(html)
499
+ const source = $('main').html()
500
+ if (!source) {
501
+ return {
502
+ content: [],
503
+ }
458
504
  }
459
- }
460
505
 
461
- const text = turndownService.turndown(source)
506
+ const text = turndownService.turndown(source)
462
507
 
463
- return {
464
- content: [
465
- {
466
- type: 'text',
467
- text: `Here is the documentation for color usage in Primer:\n\n${text}`,
468
- },
469
- ],
470
- }
471
- })
508
+ return {
509
+ content: [
510
+ {
511
+ type: 'text',
512
+ text: `Here is the documentation for color usage in Primer:\n\n${text}`,
513
+ },
514
+ ],
515
+ }
516
+ },
517
+ )
472
518
 
473
- server.tool('get_typography_usage', 'Get the guidelines for how to apply typography to a user interface', async () => {
474
- const url = new URL(`/product/getting-started/foundations/typography`, 'https://primer.style')
475
- const response = await fetch(url)
476
- if (!response.ok) {
477
- throw new Error(`Failed to fetch ${url} - ${response.statusText}`)
478
- }
519
+ server.registerTool(
520
+ 'get_typography_usage',
521
+ {description: 'Get the guidelines for how to apply typography to a user interface'},
522
+ async () => {
523
+ const url = new URL(`/product/getting-started/foundations/typography`, 'https://primer.style')
524
+ const response = await fetch(url)
525
+ if (!response.ok) {
526
+ throw new Error(`Failed to fetch ${url} - ${response.statusText}`)
527
+ }
479
528
 
480
- const html = await response.text()
481
- if (!html) {
482
- return {
483
- content: [],
529
+ const html = await response.text()
530
+ if (!html) {
531
+ return {
532
+ content: [],
533
+ }
484
534
  }
485
- }
486
535
 
487
- const $ = cheerio.load(html)
488
- const source = $('main').html()
489
- if (!source) {
490
- return {
491
- content: [],
536
+ const $ = cheerio.load(html)
537
+ const source = $('main').html()
538
+ if (!source) {
539
+ return {
540
+ content: [],
541
+ }
492
542
  }
493
- }
494
543
 
495
- const text = turndownService.turndown(source)
544
+ const text = turndownService.turndown(source)
496
545
 
497
- return {
498
- content: [
499
- {
500
- type: 'text',
501
- text: `Here is the documentation for typography usage in Primer:\n\n${text}`,
502
- },
503
- ],
504
- }
505
- })
546
+ return {
547
+ content: [
548
+ {
549
+ type: 'text',
550
+ text: `Here is the documentation for typography usage in Primer:\n\n${text}`,
551
+ },
552
+ ],
553
+ }
554
+ },
555
+ )
506
556
 
507
557
  // -----------------------------------------------------------------------------
508
558
  // Icons
509
559
  // -----------------------------------------------------------------------------
510
- server.tool('list_icons', 'List all of the icons (octicons) available from Primer Octicons React', async () => {
511
- const icons = listIcons().map(icon => {
512
- const keywords = icon.keywords.map(keyword => {
513
- return `<keyword>${keyword}</keyword>`
514
- })
515
- const sizes = icon.heights.map(height => {
516
- return `<size value="${height}"></size>`
560
+ server.registerTool(
561
+ 'list_icons',
562
+ {description: 'List all of the icons (octicons) available from Primer Octicons React'},
563
+ async () => {
564
+ const icons = listIcons().map(icon => {
565
+ const keywords = icon.keywords.map(keyword => {
566
+ return `<keyword>${keyword}</keyword>`
567
+ })
568
+ const sizes = icon.heights.map(height => {
569
+ return `<size value="${height}"></size>`
570
+ })
571
+ return [`<icon name="${icon.name}">`, ...keywords, ...sizes, `</icon>`].join('\n')
517
572
  })
518
- return [`<icon name="${icon.name}">`, ...keywords, ...sizes, `</icon>`].join('\n')
519
- })
520
573
 
521
- return {
522
- content: [
523
- {
524
- type: 'text',
525
- text: `The following icons are available in the @primer/octicons-react package in TypeScript projects:
574
+ return {
575
+ content: [
576
+ {
577
+ type: 'text',
578
+ text: `The following icons are available in the @primer/octicons-react package in TypeScript projects:
526
579
 
527
580
  ${icons.join('\n')}
528
581
 
529
582
  You can use the \`get_icon\` tool to get more information about a specific icon. You can use these components from the @primer/octicons-react package.`,
530
- },
531
- ],
532
- }
533
- })
583
+ },
584
+ ],
585
+ }
586
+ },
587
+ )
534
588
 
535
- server.tool(
589
+ server.registerTool(
536
590
  'get_icon',
537
- 'Get a specific icon (octicon) by name from Primer',
538
591
  {
539
- name: z.string().describe('The name of the icon to retrieve'),
540
- size: z.string().optional().describe('The size of the icon to retrieve, e.g. "16"').default('16'),
592
+ description: 'Get a specific icon (octicon) by name from Primer',
593
+ inputSchema: {
594
+ name: z.string().describe('The name of the icon to retrieve'),
595
+ size: z.string().optional().describe('The size of the icon to retrieve, e.g. "16"').default('16'),
596
+ },
541
597
  },
542
598
  async ({name, size}) => {
543
599
  const icons = listIcons()
@@ -593,9 +649,9 @@ ${text}`,
593
649
  // -----------------------------------------------------------------------------
594
650
  // Coding guidelines
595
651
  // -----------------------------------------------------------------------------
596
- server.tool(
652
+ server.registerTool(
597
653
  'primer_coding_guidelines',
598
- 'Get the guidelines when writing code that uses Primer or for UI code that you are creating',
654
+ {description: 'Get the guidelines when writing code that uses Primer or for UI code that you are creating'},
599
655
  async () => {
600
656
  return {
601
657
  content: [
@@ -643,19 +699,15 @@ The following list of coding guidelines must be followed:
643
699
  *
644
700
  *
645
701
  **/
646
- server.tool(
702
+ server.registerTool(
647
703
  'review_alt_text',
648
- 'Evaluates image alt text against accessibility best practices and context relevance.',
649
704
  {
650
- surroundingText: z.string().describe('Text surrounding the image, relevant to the image.'),
651
- alt: z.string().describe('The alt text of the image being evaluated'),
652
- image: z
653
- .union([
654
- z.instanceof(File).describe('The image src file being evaluated'),
655
- z.string().url().describe('The URL of the image src being evaluated'),
656
- z.string().describe('The file path of the image src being evaluated'),
657
- ])
658
- .describe('The image file, file path, or URL being evaluated'),
705
+ description: 'Evaluates image alt text against accessibility best practices and context relevance.',
706
+ inputSchema: {
707
+ surroundingText: z.string().describe('Text surrounding the image, relevant to the image.'),
708
+ alt: z.string().describe('The alt text of the image being evaluated'),
709
+ image: z.string().describe('The image URL or file path being evaluated'),
710
+ },
659
711
  },
660
712
  async ({surroundingText, alt, image}) => {
661
713
  // Call the LLM through MCP sampling