@primer/mcp 0.5.1 → 1.0.0-rc.f27c5cca1
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/README.md +2 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -2
- package/dist/{server-Dv5XFnmA.js → server-MtARfEmu.js} +443 -434
- package/dist/stdio.js +3 -4
- package/package.json +7 -3
- package/src/index.ts +1 -1
- package/src/server.test.ts +37 -4
- package/src/server.ts +853 -833
- package/src/transports/stdio.ts +3 -5
package/src/server.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {McpServer} from '@modelcontextprotocol/
|
|
1
|
+
import {inputRequired, inputResponse, McpServer} from '@modelcontextprotocol/server'
|
|
2
|
+
|
|
2
3
|
// eslint-disable-next-line import/no-namespace
|
|
3
4
|
import * as cheerio from 'cheerio'
|
|
4
5
|
// eslint-disable-next-line import/no-namespace
|
|
@@ -22,54 +23,55 @@ import {
|
|
|
22
23
|
} from './primitives'
|
|
23
24
|
import packageJson from '../package.json' with {type: 'json'}
|
|
24
25
|
|
|
25
|
-
const server = new McpServer({
|
|
26
|
-
name: 'Primer',
|
|
27
|
-
version: packageJson.version,
|
|
28
|
-
})
|
|
29
|
-
|
|
30
26
|
const turndownService = new TurndownService()
|
|
31
27
|
|
|
32
28
|
// Load all tokens with guidelines from primitives
|
|
33
29
|
const allTokensWithGuidelines: TokenWithGuidelines[] = loadAllTokensWithGuidelines()
|
|
34
30
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
31
|
+
function createServer() {
|
|
32
|
+
const server = new McpServer({
|
|
33
|
+
name: 'Primer',
|
|
34
|
+
version: packageJson.version,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// -----------------------------------------------------------------------------
|
|
38
|
+
// Project setup
|
|
39
|
+
// -----------------------------------------------------------------------------
|
|
40
|
+
server.registerTool(
|
|
41
|
+
'init',
|
|
42
|
+
{
|
|
43
|
+
description: 'Setup or create a project that includes Primer React',
|
|
44
|
+
annotations: {readOnlyHint: true},
|
|
45
|
+
},
|
|
46
|
+
async () => {
|
|
47
|
+
const url = new URL(`/product/getting-started/react`, 'https://primer.style')
|
|
48
|
+
const response = await fetch(url)
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
throw new Error(`Failed to fetch ${url}: ${response.statusText}`)
|
|
55
51
|
}
|
|
56
|
-
}
|
|
57
52
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
53
|
+
const html = await response.text()
|
|
54
|
+
if (!html) {
|
|
55
|
+
return {
|
|
56
|
+
content: [],
|
|
57
|
+
}
|
|
63
58
|
}
|
|
64
|
-
}
|
|
65
59
|
|
|
66
|
-
|
|
60
|
+
const $ = cheerio.load(html)
|
|
61
|
+
const source = $('main').html()
|
|
62
|
+
if (!source) {
|
|
63
|
+
return {
|
|
64
|
+
content: [],
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
const text = turndownService.turndown(source)
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
content: [
|
|
72
|
+
{
|
|
73
|
+
type: 'text',
|
|
74
|
+
text: `The getting started documentation for Primer React is included below. It's important that the project:
|
|
73
75
|
|
|
74
76
|
- 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
|
|
75
77
|
- Installs the latest version of \`@primer/react\` from \`npm\`
|
|
@@ -82,895 +84,895 @@ server.registerTool(
|
|
|
82
84
|
|
|
83
85
|
${text}
|
|
84
86
|
`,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
// -----------------------------------------------------------------------------
|
|
92
|
-
// Components
|
|
93
|
-
// -----------------------------------------------------------------------------
|
|
94
|
-
server.registerTool(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
// -----------------------------------------------------------------------------
|
|
94
|
+
// Components
|
|
95
|
+
// -----------------------------------------------------------------------------
|
|
96
|
+
server.registerTool(
|
|
97
|
+
'list_components',
|
|
98
|
+
{description: 'List all of the components available from Primer React', annotations: {readOnlyHint: true}},
|
|
99
|
+
async () => {
|
|
100
|
+
const components = listComponents().map(component => {
|
|
101
|
+
return `- ${component.name}`
|
|
102
|
+
})
|
|
103
|
+
return {
|
|
104
|
+
content: [
|
|
105
|
+
{
|
|
106
|
+
type: 'text',
|
|
107
|
+
text: `The following components are available in the @primer/react in TypeScript projects:
|
|
106
108
|
|
|
107
109
|
${components.join('\n')}
|
|
108
110
|
|
|
109
111
|
Use \`get_component\` for exactly one component and \`get_component_batch\` for 2 to 10 components. You can use these components from the @primer/react package.`,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
},
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
server.registerTool(
|
|
117
|
-
'get_component',
|
|
118
|
-
{
|
|
119
|
-
description:
|
|
120
|
-
'Retrieve official documentation and usage details for exactly one React component from the @primer/react package. Use get_component_batch for 2 to 10 components.',
|
|
121
|
-
inputSchema: {
|
|
122
|
-
name: z.string().describe('The name of the component to retrieve'),
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
}
|
|
123
115
|
},
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
server.registerTool(
|
|
119
|
+
'get_component',
|
|
120
|
+
{
|
|
121
|
+
description:
|
|
122
|
+
'Retrieve official documentation and usage details for exactly one React component from the @primer/react package. Use get_component_batch for 2 to 10 components.',
|
|
123
|
+
inputSchema: z.object({
|
|
124
|
+
name: z.string().describe('The name of the component to retrieve'),
|
|
125
|
+
}),
|
|
126
|
+
annotations: {readOnlyHint: true},
|
|
127
|
+
},
|
|
128
|
+
async ({name}) => {
|
|
129
|
+
const components = listComponents()
|
|
130
|
+
const match = components.find(component => {
|
|
131
|
+
return component.name === name || component.name.toLowerCase() === name.toLowerCase()
|
|
132
|
+
})
|
|
133
|
+
if (!match) {
|
|
134
|
+
return {
|
|
135
|
+
isError: true,
|
|
136
|
+
errorMessage: `There is no component named \`${name}\` in the @primer/react package. For a full list of components, use the \`list_components\` tool.`,
|
|
137
|
+
content: [],
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const llmsUrl = new URL(`/product/components/${match.slug}/llms.txt`, 'https://primer.style')
|
|
142
|
+
const llmsResponse = await fetch(llmsUrl)
|
|
143
|
+
if (llmsResponse.ok) {
|
|
144
|
+
const llmsText = await llmsResponse.text()
|
|
145
|
+
return {
|
|
146
|
+
content: [
|
|
147
|
+
{
|
|
148
|
+
type: 'text',
|
|
149
|
+
text: llmsText,
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} catch (_: unknown) {
|
|
155
|
+
// If there's an error fetching or processing the llms.txt, we fall back to a generic error message.
|
|
156
|
+
}
|
|
157
|
+
|
|
132
158
|
return {
|
|
133
159
|
isError: true,
|
|
134
|
-
errorMessage: `There
|
|
160
|
+
errorMessage: `There was an error fetching documentation for ${name}. Ensure the component exists.`,
|
|
135
161
|
content: [],
|
|
136
162
|
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
163
|
+
},
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
server.registerTool(
|
|
167
|
+
'get_component_batch',
|
|
168
|
+
{
|
|
169
|
+
description:
|
|
170
|
+
'Retrieve official documentation and usage details for 2 to 10 React components from the @primer/react package in one call. Use get_component for exactly one component.',
|
|
171
|
+
inputSchema: z.object({
|
|
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
|
+
|
|
231
|
+
server.registerTool(
|
|
232
|
+
'get_component_examples',
|
|
233
|
+
{
|
|
234
|
+
description: 'Get examples for how to use a component from Primer React',
|
|
235
|
+
inputSchema: z.object({
|
|
236
|
+
name: z.string().describe('The name of the component to retrieve'),
|
|
237
|
+
}),
|
|
238
|
+
annotations: {readOnlyHint: true},
|
|
239
|
+
},
|
|
240
|
+
async ({name}) => {
|
|
241
|
+
const components = listComponents()
|
|
242
|
+
const match = components.find(component => {
|
|
243
|
+
return component.name === name
|
|
244
|
+
})
|
|
245
|
+
if (!match) {
|
|
143
246
|
return {
|
|
144
247
|
content: [
|
|
145
248
|
{
|
|
146
249
|
type: 'text',
|
|
147
|
-
text:
|
|
250
|
+
text: `There is no component named \`${name}\` in the @primer/react package. For a full list of components, use the \`list_components\` tool.`,
|
|
148
251
|
},
|
|
149
252
|
],
|
|
150
253
|
}
|
|
151
254
|
}
|
|
152
|
-
} catch (_: unknown) {
|
|
153
|
-
// If there's an error fetching or processing the llms.txt, we fall back to a generic error message.
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
isError: true,
|
|
158
|
-
errorMessage: `There was an error fetching documentation for ${name}. Ensure the component exists.`,
|
|
159
|
-
content: [],
|
|
160
|
-
}
|
|
161
|
-
},
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
server.registerTool(
|
|
165
|
-
'get_component_batch',
|
|
166
|
-
{
|
|
167
|
-
description:
|
|
168
|
-
'Retrieve official documentation and usage details for 2 to 10 React components from the @primer/react package in one call. Use get_component for exactly one component.',
|
|
169
|
-
inputSchema: {
|
|
170
|
-
names: z
|
|
171
|
-
.array(z.string())
|
|
172
|
-
.min(2)
|
|
173
|
-
.max(10)
|
|
174
|
-
.describe('Component names to retrieve (e.g. ["ActionMenu", "Button", "Pagination"])'),
|
|
175
|
-
},
|
|
176
|
-
annotations: {readOnlyHint: true},
|
|
177
|
-
},
|
|
178
|
-
async ({names}) => {
|
|
179
|
-
const seenNames = new Set<string>()
|
|
180
|
-
const deduped = names.filter(name => {
|
|
181
|
-
const normalizedName = name.toLowerCase()
|
|
182
|
-
if (seenNames.has(normalizedName)) {
|
|
183
|
-
return false
|
|
184
|
-
}
|
|
185
|
-
seenNames.add(normalizedName)
|
|
186
|
-
return true
|
|
187
|
-
})
|
|
188
|
-
const components = listComponents()
|
|
189
|
-
|
|
190
|
-
const results = await Promise.all(
|
|
191
|
-
deduped.map(async name => {
|
|
192
|
-
const match = components.find(
|
|
193
|
-
component => component.name === name || component.name.toLowerCase() === name.toLowerCase(),
|
|
194
|
-
)
|
|
195
|
-
if (!match) {
|
|
196
|
-
return {
|
|
197
|
-
type: 'text' as const,
|
|
198
|
-
text: `## ${name}\n\nStatus: not-found. No component named \`${name}\` exists in @primer/react. Use \`list_components\` for valid names.`,
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
255
|
|
|
202
|
-
|
|
203
|
-
|
|
256
|
+
const url = new URL(`/product/components/${match.id}`, 'https://primer.style')
|
|
257
|
+
const response = await fetch(url)
|
|
258
|
+
if (!response.ok) {
|
|
259
|
+
throw new Error(`Failed to fetch ${url}: ${response.statusText}`)
|
|
260
|
+
}
|
|
204
261
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const text = await llmsResponse.text()
|
|
210
|
-
return {type: 'text' as const, text}
|
|
211
|
-
}
|
|
212
|
-
} catch (_: unknown) {
|
|
213
|
-
// Fall through to the in-band error result so other component requests can succeed.
|
|
214
|
-
} finally {
|
|
215
|
-
clearTimeout(timeout)
|
|
262
|
+
const html = await response.text()
|
|
263
|
+
if (!html) {
|
|
264
|
+
return {
|
|
265
|
+
content: [],
|
|
216
266
|
}
|
|
267
|
+
}
|
|
217
268
|
|
|
269
|
+
const $ = cheerio.load(html)
|
|
270
|
+
const source = $('main').html()
|
|
271
|
+
if (!source) {
|
|
218
272
|
return {
|
|
219
|
-
|
|
220
|
-
text: `## ${match.name}\n\nStatus: fetch-error. Failed to load documentation for ${match.name}.`,
|
|
273
|
+
content: [],
|
|
221
274
|
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
},
|
|
227
|
-
)
|
|
228
|
-
|
|
229
|
-
server.registerTool(
|
|
230
|
-
'get_component_examples',
|
|
231
|
-
{
|
|
232
|
-
description: 'Get examples for how to use a component from Primer React',
|
|
233
|
-
inputSchema: {
|
|
234
|
-
name: z.string().describe('The name of the component to retrieve'),
|
|
235
|
-
},
|
|
236
|
-
annotations: {readOnlyHint: true},
|
|
237
|
-
},
|
|
238
|
-
async ({name}) => {
|
|
239
|
-
const components = listComponents()
|
|
240
|
-
const match = components.find(component => {
|
|
241
|
-
return component.name === name
|
|
242
|
-
})
|
|
243
|
-
if (!match) {
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const text = turndownService.turndown(source)
|
|
278
|
+
|
|
244
279
|
return {
|
|
245
280
|
content: [
|
|
246
281
|
{
|
|
247
282
|
type: 'text',
|
|
248
|
-
text: `
|
|
283
|
+
text: `Here are some examples of how to use the \`${name}\` component from the @primer/react package:
|
|
284
|
+
|
|
285
|
+
${text}`,
|
|
249
286
|
},
|
|
250
287
|
],
|
|
251
288
|
}
|
|
252
|
-
}
|
|
289
|
+
},
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
server.registerTool(
|
|
293
|
+
'get_component_usage_guidelines',
|
|
294
|
+
{
|
|
295
|
+
description: 'Get usage information for how to use a component from Primer',
|
|
296
|
+
inputSchema: z.object({
|
|
297
|
+
name: z.string().describe('The name of the component to retrieve'),
|
|
298
|
+
}),
|
|
299
|
+
annotations: {readOnlyHint: true},
|
|
300
|
+
},
|
|
301
|
+
async ({name}) => {
|
|
302
|
+
const components = listComponents()
|
|
303
|
+
const match = components.find(component => {
|
|
304
|
+
return component.name === name
|
|
305
|
+
})
|
|
306
|
+
if (!match) {
|
|
307
|
+
return {
|
|
308
|
+
content: [
|
|
309
|
+
{
|
|
310
|
+
type: 'text',
|
|
311
|
+
text: `There is no component named \`${name}\` in the @primer/react package. For a full list of components, use the \`get_components\` tool.`,
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
}
|
|
315
|
+
}
|
|
253
316
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
317
|
+
const url = new URL(`/product/components/${match.id}/guidelines`, 'https://primer.style')
|
|
318
|
+
const response = await fetch(url)
|
|
319
|
+
if (!response.ok) {
|
|
320
|
+
if ((response.status >= 400 && response.status < 500) || (response.status >= 300 && response.status < 400)) {
|
|
321
|
+
return {
|
|
322
|
+
content: [
|
|
323
|
+
{
|
|
324
|
+
type: 'text',
|
|
325
|
+
text: `There are no accessibility guidelines for the \`${name}\` component in the @primer/react package.`,
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
}
|
|
329
|
+
}
|
|
259
330
|
|
|
260
|
-
|
|
261
|
-
if (!html) {
|
|
262
|
-
return {
|
|
263
|
-
content: [],
|
|
331
|
+
throw new Error(`Failed to fetch ${url}: ${response.statusText}`)
|
|
264
332
|
}
|
|
265
|
-
}
|
|
266
333
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
334
|
+
const html = await response.text()
|
|
335
|
+
if (!html) {
|
|
336
|
+
return {
|
|
337
|
+
content: [],
|
|
338
|
+
}
|
|
272
339
|
}
|
|
273
|
-
}
|
|
274
340
|
|
|
275
|
-
|
|
341
|
+
const $ = cheerio.load(html)
|
|
342
|
+
const source = $('main').html()
|
|
343
|
+
if (!source) {
|
|
344
|
+
return {
|
|
345
|
+
content: [],
|
|
346
|
+
}
|
|
347
|
+
}
|
|
276
348
|
|
|
277
|
-
|
|
278
|
-
content: [
|
|
279
|
-
{
|
|
280
|
-
type: 'text',
|
|
281
|
-
text: `Here are some examples of how to use the \`${name}\` component from the @primer/react package:
|
|
349
|
+
const text = turndownService.turndown(source)
|
|
282
350
|
|
|
283
|
-
${text}`,
|
|
284
|
-
},
|
|
285
|
-
],
|
|
286
|
-
}
|
|
287
|
-
},
|
|
288
|
-
)
|
|
289
|
-
|
|
290
|
-
server.registerTool(
|
|
291
|
-
'get_component_usage_guidelines',
|
|
292
|
-
{
|
|
293
|
-
description: 'Get usage information for how to use a component from Primer',
|
|
294
|
-
inputSchema: {
|
|
295
|
-
name: z.string().describe('The name of the component to retrieve'),
|
|
296
|
-
},
|
|
297
|
-
annotations: {readOnlyHint: true},
|
|
298
|
-
},
|
|
299
|
-
async ({name}) => {
|
|
300
|
-
const components = listComponents()
|
|
301
|
-
const match = components.find(component => {
|
|
302
|
-
return component.name === name
|
|
303
|
-
})
|
|
304
|
-
if (!match) {
|
|
305
351
|
return {
|
|
306
352
|
content: [
|
|
307
353
|
{
|
|
308
354
|
type: 'text',
|
|
309
|
-
text: `
|
|
355
|
+
text: `Here are the usage guidelines for the \`${name}\` component from the @primer/react package:
|
|
356
|
+
|
|
357
|
+
${text}`,
|
|
310
358
|
},
|
|
311
359
|
],
|
|
312
360
|
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
361
|
+
},
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
server.registerTool(
|
|
365
|
+
'get_component_accessibility_guidelines',
|
|
366
|
+
{
|
|
367
|
+
description:
|
|
368
|
+
'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.',
|
|
369
|
+
inputSchema: z.object({
|
|
370
|
+
name: z.string().describe('The name of the component to retrieve'),
|
|
371
|
+
}),
|
|
372
|
+
annotations: {readOnlyHint: true},
|
|
373
|
+
},
|
|
374
|
+
async ({name}) => {
|
|
375
|
+
const components = listComponents()
|
|
376
|
+
const match = components.find(component => {
|
|
377
|
+
return component.name === name
|
|
378
|
+
})
|
|
379
|
+
if (!match) {
|
|
319
380
|
return {
|
|
320
381
|
content: [
|
|
321
382
|
{
|
|
322
383
|
type: 'text',
|
|
323
|
-
text: `There
|
|
384
|
+
text: `There is no component named \`${name}\` in the @primer/react package. For a full list of components, use the \`list_components\` tool.`,
|
|
324
385
|
},
|
|
325
386
|
],
|
|
326
387
|
}
|
|
327
388
|
}
|
|
328
389
|
|
|
329
|
-
|
|
330
|
-
|
|
390
|
+
const url = new URL(`/product/components/${match.id}/accessibility`, 'https://primer.style')
|
|
391
|
+
const response = await fetch(url)
|
|
392
|
+
if (!response.ok) {
|
|
393
|
+
if ((response.status >= 400 && response.status < 500) || (response.status >= 300 && response.status < 400)) {
|
|
394
|
+
return {
|
|
395
|
+
content: [
|
|
396
|
+
{
|
|
397
|
+
type: 'text',
|
|
398
|
+
text: `There are no accessibility guidelines for the \`${name}\` component in the @primer/react package.`,
|
|
399
|
+
},
|
|
400
|
+
],
|
|
401
|
+
}
|
|
402
|
+
}
|
|
331
403
|
|
|
332
|
-
|
|
333
|
-
if (!html) {
|
|
334
|
-
return {
|
|
335
|
-
content: [],
|
|
404
|
+
throw new Error(`Failed to fetch ${url}: ${response.statusText}`)
|
|
336
405
|
}
|
|
337
|
-
}
|
|
338
406
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
407
|
+
const html = await response.text()
|
|
408
|
+
if (!html) {
|
|
409
|
+
return {
|
|
410
|
+
content: [],
|
|
411
|
+
}
|
|
344
412
|
}
|
|
345
|
-
}
|
|
346
413
|
|
|
347
|
-
|
|
414
|
+
const $ = cheerio.load(html)
|
|
415
|
+
const source = $('main').html()
|
|
416
|
+
if (!source) {
|
|
417
|
+
return {
|
|
418
|
+
content: [],
|
|
419
|
+
}
|
|
420
|
+
}
|
|
348
421
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
422
|
+
const text = turndownService.turndown(source)
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
content: [
|
|
426
|
+
{
|
|
427
|
+
type: 'text',
|
|
428
|
+
text: `Here are the accessibility guidelines for the \`${name}\` component from the @primer/react package:
|
|
354
429
|
|
|
355
430
|
${text}`,
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
431
|
+
},
|
|
432
|
+
],
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
// -----------------------------------------------------------------------------
|
|
438
|
+
// Patterns
|
|
439
|
+
// -----------------------------------------------------------------------------
|
|
440
|
+
server.registerTool(
|
|
441
|
+
'list_patterns',
|
|
442
|
+
{
|
|
443
|
+
description:
|
|
444
|
+
'List all of the patterns available from Primer React. Scenario patterns describe specific user tasks (copy, delete, filter, search). Prefer a scenario pattern when one fits the task, and fall back to the more generic UI patterns otherwise.',
|
|
445
|
+
annotations: {readOnlyHint: true},
|
|
369
446
|
},
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const match = components.find(component => {
|
|
375
|
-
return component.name === name
|
|
376
|
-
})
|
|
377
|
-
if (!match) {
|
|
447
|
+
async () => {
|
|
448
|
+
const all = listPatterns()
|
|
449
|
+
const scenario = all.filter(pattern => pattern.category === 'scenario').map(pattern => `- ${pattern.name}`)
|
|
450
|
+
const ui = all.filter(pattern => pattern.category === 'ui').map(pattern => `- ${pattern.name}`)
|
|
378
451
|
return {
|
|
379
452
|
content: [
|
|
380
453
|
{
|
|
381
454
|
type: 'text',
|
|
382
|
-
text: `
|
|
455
|
+
text: `The following patterns are available from \`@primer/react\` for use in TypeScript projects. Scenario patterns describe specific user tasks. Prefer a scenario pattern when one fits the task, and fall back to the UI patterns otherwise.
|
|
456
|
+
|
|
457
|
+
## Scenario patterns
|
|
458
|
+
|
|
459
|
+
${scenario.join('\n')}
|
|
460
|
+
|
|
461
|
+
## UI patterns
|
|
462
|
+
|
|
463
|
+
${ui.join('\n')}`,
|
|
383
464
|
},
|
|
384
465
|
],
|
|
385
466
|
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
467
|
+
},
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
server.registerTool(
|
|
471
|
+
'get_pattern',
|
|
472
|
+
{
|
|
473
|
+
description:
|
|
474
|
+
'Get a specific pattern by name. Scenario patterns describe specific user tasks (copy, delete, filter, search). Prefer a scenario pattern when one fits the task, and fall back to the more generic UI patterns otherwise.',
|
|
475
|
+
inputSchema: z.object({
|
|
476
|
+
name: z.string().describe('The name of the pattern to retrieve'),
|
|
477
|
+
}),
|
|
478
|
+
annotations: {readOnlyHint: true},
|
|
479
|
+
},
|
|
480
|
+
async ({name}) => {
|
|
481
|
+
const patterns = listPatterns()
|
|
482
|
+
// Resolve scenario patterns first so a name clash favours the scenario pattern.
|
|
483
|
+
const match =
|
|
484
|
+
patterns.find(pattern => pattern.category === 'scenario' && pattern.name === name) ??
|
|
485
|
+
patterns.find(pattern => pattern.name === name)
|
|
486
|
+
if (!match) {
|
|
392
487
|
return {
|
|
393
488
|
content: [
|
|
394
489
|
{
|
|
395
490
|
type: 'text',
|
|
396
|
-
text: `There
|
|
491
|
+
text: `There is no pattern named \`${name}\` in the @primer/react package. For a full list of patterns, use the \`list_patterns\` tool.`,
|
|
397
492
|
},
|
|
398
493
|
],
|
|
399
494
|
}
|
|
400
495
|
}
|
|
401
496
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
return {
|
|
408
|
-
content: [],
|
|
497
|
+
const basePath = match.category === 'scenario' ? 'scenario-patterns' : 'ui-patterns'
|
|
498
|
+
const url = new URL(`/product/${basePath}/${match.id}`, 'https://primer.style')
|
|
499
|
+
const response = await fetch(url)
|
|
500
|
+
if (!response.ok) {
|
|
501
|
+
throw new Error(`Failed to fetch ${url} - ${response.statusText}`)
|
|
409
502
|
}
|
|
410
|
-
}
|
|
411
503
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
504
|
+
const html = await response.text()
|
|
505
|
+
if (!html) {
|
|
506
|
+
return {
|
|
507
|
+
content: [],
|
|
508
|
+
}
|
|
417
509
|
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const text = turndownService.turndown(source)
|
|
421
510
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
},
|
|
430
|
-
],
|
|
431
|
-
}
|
|
432
|
-
},
|
|
433
|
-
)
|
|
434
|
-
|
|
435
|
-
// -----------------------------------------------------------------------------
|
|
436
|
-
// Patterns
|
|
437
|
-
// -----------------------------------------------------------------------------
|
|
438
|
-
server.registerTool(
|
|
439
|
-
'list_patterns',
|
|
440
|
-
{
|
|
441
|
-
description:
|
|
442
|
-
'List all of the patterns available from Primer React. Scenario patterns describe specific user tasks (copy, delete, filter, search). Prefer a scenario pattern when one fits the task, and fall back to the more generic UI patterns otherwise.',
|
|
443
|
-
annotations: {readOnlyHint: true},
|
|
444
|
-
},
|
|
445
|
-
async () => {
|
|
446
|
-
const all = listPatterns()
|
|
447
|
-
const scenario = all.filter(pattern => pattern.category === 'scenario').map(pattern => `- ${pattern.name}`)
|
|
448
|
-
const ui = all.filter(pattern => pattern.category === 'ui').map(pattern => `- ${pattern.name}`)
|
|
449
|
-
return {
|
|
450
|
-
content: [
|
|
451
|
-
{
|
|
452
|
-
type: 'text',
|
|
453
|
-
text: `The following patterns are available from \`@primer/react\` for use in TypeScript projects. Scenario patterns describe specific user tasks. Prefer a scenario pattern when one fits the task, and fall back to the UI patterns otherwise.
|
|
454
|
-
|
|
455
|
-
## Scenario patterns
|
|
456
|
-
|
|
457
|
-
${scenario.join('\n')}
|
|
511
|
+
const $ = cheerio.load(html)
|
|
512
|
+
const source = $('main').html()
|
|
513
|
+
if (!source) {
|
|
514
|
+
return {
|
|
515
|
+
content: [],
|
|
516
|
+
}
|
|
517
|
+
}
|
|
458
518
|
|
|
459
|
-
|
|
519
|
+
const text = turndownService.turndown(source)
|
|
460
520
|
|
|
461
|
-
${ui.join('\n')}`,
|
|
462
|
-
},
|
|
463
|
-
],
|
|
464
|
-
}
|
|
465
|
-
},
|
|
466
|
-
)
|
|
467
|
-
|
|
468
|
-
server.registerTool(
|
|
469
|
-
'get_pattern',
|
|
470
|
-
{
|
|
471
|
-
description:
|
|
472
|
-
'Get a specific pattern by name. Scenario patterns describe specific user tasks (copy, delete, filter, search). Prefer a scenario pattern when one fits the task, and fall back to the more generic UI patterns otherwise.',
|
|
473
|
-
inputSchema: {
|
|
474
|
-
name: z.string().describe('The name of the pattern to retrieve'),
|
|
475
|
-
},
|
|
476
|
-
annotations: {readOnlyHint: true},
|
|
477
|
-
},
|
|
478
|
-
async ({name}) => {
|
|
479
|
-
const patterns = listPatterns()
|
|
480
|
-
// Resolve scenario patterns first so a name clash favours the scenario pattern.
|
|
481
|
-
const match =
|
|
482
|
-
patterns.find(pattern => pattern.category === 'scenario' && pattern.name === name) ??
|
|
483
|
-
patterns.find(pattern => pattern.name === name)
|
|
484
|
-
if (!match) {
|
|
485
521
|
return {
|
|
486
522
|
content: [
|
|
487
523
|
{
|
|
488
524
|
type: 'text',
|
|
489
|
-
text: `
|
|
525
|
+
text: `Here are the guidelines for the \`${name}\` pattern for Primer:
|
|
526
|
+
|
|
527
|
+
${text}`,
|
|
490
528
|
},
|
|
491
529
|
],
|
|
492
530
|
}
|
|
493
|
-
}
|
|
531
|
+
},
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
// -----------------------------------------------------------------------------
|
|
535
|
+
// Design Tokens
|
|
536
|
+
// -----------------------------------------------------------------------------
|
|
537
|
+
server.registerTool(
|
|
538
|
+
'find_tokens',
|
|
539
|
+
{
|
|
540
|
+
description:
|
|
541
|
+
'Search for specific tokens. Tip: If you only provide a \'group\' and leave \'query\' empty, it returns all tokens in that category. Avoid property-by-property searching. COLOR RESOLUTION: If a user asks for "pink" or "blue", do not search for the color name. Use the semantic intent: blue->accent, red->danger, green->success. Always check both "emphasis" and "muted" variants for background colors. After identifying tokens and writing CSS, you MUST validate the result using lint_css.',
|
|
542
|
+
inputSchema: z.object({
|
|
543
|
+
query: z
|
|
544
|
+
.string()
|
|
545
|
+
.optional()
|
|
546
|
+
.default('')
|
|
547
|
+
.describe('Search keywords (e.g., "danger border", "success background")'),
|
|
548
|
+
group: z.string().optional().describe('Filter by group (e.g., "fgColor", "border")'),
|
|
549
|
+
limit: z
|
|
550
|
+
.number()
|
|
551
|
+
.int()
|
|
552
|
+
.min(1)
|
|
553
|
+
.max(100)
|
|
554
|
+
.optional()
|
|
555
|
+
.default(15)
|
|
556
|
+
.describe('Maximum results to return to stay within context limits'),
|
|
557
|
+
}),
|
|
558
|
+
annotations: {readOnlyHint: true},
|
|
559
|
+
},
|
|
560
|
+
async ({query, group, limit}) => {
|
|
561
|
+
// Resolve group via aliases
|
|
562
|
+
const resolvedGroup = group ? GROUP_ALIASES[group.toLowerCase().replace(/\s+/g, '')] || group : undefined
|
|
563
|
+
|
|
564
|
+
// Split query into keywords and extract any that match a known group
|
|
565
|
+
const rawKeywords = query
|
|
566
|
+
.toLowerCase()
|
|
567
|
+
.split(/\s+/)
|
|
568
|
+
.filter(k => k.length > 0)
|
|
569
|
+
|
|
570
|
+
let effectiveGroup = resolvedGroup
|
|
571
|
+
const filteredKeywords: string[] = []
|
|
572
|
+
|
|
573
|
+
for (const kw of rawKeywords) {
|
|
574
|
+
const normalized = kw.replace(/\s+/g, '')
|
|
575
|
+
const aliasMatch = GROUP_ALIASES[normalized]
|
|
576
|
+
if (aliasMatch && !effectiveGroup) {
|
|
577
|
+
effectiveGroup = aliasMatch
|
|
578
|
+
} else {
|
|
579
|
+
filteredKeywords.push(kw)
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Guard: no query and no group → ask user to provide at least one
|
|
584
|
+
if (filteredKeywords.length === 0 && !effectiveGroup) {
|
|
585
|
+
return {
|
|
586
|
+
content: [
|
|
587
|
+
{
|
|
588
|
+
type: 'text',
|
|
589
|
+
text: 'Please provide a query, a group, or both. Call `get_design_token_specs` to see available token groups.',
|
|
590
|
+
},
|
|
591
|
+
],
|
|
592
|
+
}
|
|
593
|
+
}
|
|
494
594
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
if (!response.ok) {
|
|
499
|
-
throw new Error(`Failed to fetch ${url} - ${response.statusText}`)
|
|
500
|
-
}
|
|
595
|
+
// Group-only search: return all tokens in the group
|
|
596
|
+
const isGroupOnly = filteredKeywords.length === 0 && effectiveGroup
|
|
597
|
+
let results: TokenWithGuidelines[]
|
|
501
598
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
599
|
+
if (isGroupOnly) {
|
|
600
|
+
results = allTokensWithGuidelines.filter(token => tokenMatchesGroup(token, effectiveGroup!))
|
|
601
|
+
} else {
|
|
602
|
+
results = searchTokens(allTokensWithGuidelines, filteredKeywords.join(' '), effectiveGroup)
|
|
506
603
|
}
|
|
507
|
-
}
|
|
508
604
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
605
|
+
if (results.length === 0) {
|
|
606
|
+
const validGroups = getValidGroupsList(allTokensWithGuidelines)
|
|
607
|
+
|
|
608
|
+
return {
|
|
609
|
+
content: [
|
|
610
|
+
{
|
|
611
|
+
type: 'text',
|
|
612
|
+
text: `No tokens found matching "${query}"${effectiveGroup ? ` in group "${effectiveGroup}"` : ''}.
|
|
613
|
+
|
|
614
|
+
### 💡 Available Groups:
|
|
615
|
+
${validGroups}
|
|
616
|
+
|
|
617
|
+
### Troubleshooting for AI:
|
|
618
|
+
1. **Multi-word Queries**: Search keywords use 'AND' logic. If searching "text shorthand typography" fails, try a single keyword like "shorthand" within the "text" group.
|
|
619
|
+
2. **Property Mismatch**: Do not search for CSS properties like "offset", "padding", or "font-size". Use semantic intent keywords: "danger", "muted", "emphasis".
|
|
620
|
+
3. **Typography**: Remember that \`caption\`, \`display\`, and \`code\` groups do NOT support size suffixes. Use the base shorthand only.
|
|
621
|
+
4. **Group Intent**: Use the \`group\` parameter instead of putting group names in the \`query\` string (e.g., use group: "stack" instead of query: "stack padding").`,
|
|
622
|
+
},
|
|
623
|
+
],
|
|
624
|
+
}
|
|
514
625
|
}
|
|
515
|
-
}
|
|
516
626
|
|
|
517
|
-
|
|
627
|
+
const limitedResults = results.slice(0, limit)
|
|
518
628
|
|
|
519
|
-
|
|
520
|
-
content: [
|
|
521
|
-
{
|
|
522
|
-
type: 'text',
|
|
523
|
-
text: `Here are the guidelines for the \`${name}\` pattern for Primer:
|
|
629
|
+
let output: string
|
|
524
630
|
|
|
525
|
-
|
|
526
|
-
}
|
|
527
|
-
],
|
|
528
|
-
}
|
|
529
|
-
},
|
|
530
|
-
)
|
|
531
|
-
|
|
532
|
-
// -----------------------------------------------------------------------------
|
|
533
|
-
// Design Tokens
|
|
534
|
-
// -----------------------------------------------------------------------------
|
|
535
|
-
server.registerTool(
|
|
536
|
-
'find_tokens',
|
|
537
|
-
{
|
|
538
|
-
description:
|
|
539
|
-
'Search for specific tokens. Tip: If you only provide a \'group\' and leave \'query\' empty, it returns all tokens in that category. Avoid property-by-property searching. COLOR RESOLUTION: If a user asks for "pink" or "blue", do not search for the color name. Use the semantic intent: blue->accent, red->danger, green->success. Always check both "emphasis" and "muted" variants for background colors. After identifying tokens and writing CSS, you MUST validate the result using lint_css.',
|
|
540
|
-
inputSchema: {
|
|
541
|
-
query: z
|
|
542
|
-
.string()
|
|
543
|
-
.optional()
|
|
544
|
-
.default('')
|
|
545
|
-
.describe('Search keywords (e.g., "danger border", "success background")'),
|
|
546
|
-
group: z.string().optional().describe('Filter by group (e.g., "fgColor", "border")'),
|
|
547
|
-
limit: z
|
|
548
|
-
.number()
|
|
549
|
-
.int()
|
|
550
|
-
.min(1)
|
|
551
|
-
.max(100)
|
|
552
|
-
.optional()
|
|
553
|
-
.default(15)
|
|
554
|
-
.describe('Maximum results to return to stay within context limits'),
|
|
555
|
-
},
|
|
556
|
-
annotations: {readOnlyHint: true},
|
|
557
|
-
},
|
|
558
|
-
async ({query, group, limit}) => {
|
|
559
|
-
// Resolve group via aliases
|
|
560
|
-
const resolvedGroup = group ? GROUP_ALIASES[group.toLowerCase().replace(/\s+/g, '')] || group : undefined
|
|
561
|
-
|
|
562
|
-
// Split query into keywords and extract any that match a known group
|
|
563
|
-
const rawKeywords = query
|
|
564
|
-
.toLowerCase()
|
|
565
|
-
.split(/\s+/)
|
|
566
|
-
.filter(k => k.length > 0)
|
|
567
|
-
|
|
568
|
-
let effectiveGroup = resolvedGroup
|
|
569
|
-
const filteredKeywords: string[] = []
|
|
570
|
-
|
|
571
|
-
for (const kw of rawKeywords) {
|
|
572
|
-
const normalized = kw.replace(/\s+/g, '')
|
|
573
|
-
const aliasMatch = GROUP_ALIASES[normalized]
|
|
574
|
-
if (aliasMatch && !effectiveGroup) {
|
|
575
|
-
effectiveGroup = aliasMatch
|
|
631
|
+
if (!query) {
|
|
632
|
+
output = `Found ${results.length} token(s). Showing top ${limitedResults.length}:\n\n`
|
|
576
633
|
} else {
|
|
577
|
-
|
|
634
|
+
output = `Found ${results.length} token(s) matching "${query}". Showing top ${limitedResults.length}:\n\n`
|
|
635
|
+
}
|
|
636
|
+
output += formatBundle(limitedResults)
|
|
637
|
+
|
|
638
|
+
if (results.length > limit) {
|
|
639
|
+
output += `\n\n*...and ${results.length - limit} more matches. Use more specific keywords to narrow the search.*`
|
|
578
640
|
}
|
|
579
|
-
}
|
|
580
641
|
|
|
581
|
-
// Guard: no query and no group → ask user to provide at least one
|
|
582
|
-
if (filteredKeywords.length === 0 && !effectiveGroup) {
|
|
583
642
|
return {
|
|
584
|
-
content: [
|
|
585
|
-
{
|
|
586
|
-
type: 'text',
|
|
587
|
-
text: 'Please provide a query, a group, or both. Call `get_design_token_specs` to see available token groups.',
|
|
588
|
-
},
|
|
589
|
-
],
|
|
643
|
+
content: [{type: 'text', text: output}],
|
|
590
644
|
}
|
|
591
|
-
}
|
|
645
|
+
},
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
server.registerTool(
|
|
649
|
+
'get_token_group_bundle',
|
|
650
|
+
{
|
|
651
|
+
description:
|
|
652
|
+
"PREFERRED FOR COMPONENTS. Fetch all tokens for complex UI (e.g., Dialogs, Cards) in one call by providing an array of groups like ['overlay', 'shadow']. Use this instead of multiple find_tokens calls to save context.",
|
|
653
|
+
inputSchema: z.object({
|
|
654
|
+
groups: z.array(z.string()).describe('Array of group names (e.g., ["overlay", "shadow", "focus"])'),
|
|
655
|
+
}),
|
|
656
|
+
annotations: {readOnlyHint: true},
|
|
657
|
+
},
|
|
658
|
+
async ({groups}) => {
|
|
659
|
+
// Normalize and resolve aliases
|
|
660
|
+
const resolvedGroups = groups.map(g => {
|
|
661
|
+
const normalized = g.toLowerCase().replace(/\s+/g, '')
|
|
662
|
+
return GROUP_ALIASES[normalized] || g
|
|
663
|
+
})
|
|
592
664
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
let results: TokenWithGuidelines[]
|
|
665
|
+
// Filter tokens matching any of the resolved groups
|
|
666
|
+
const matched = allTokensWithGuidelines.filter(token => resolvedGroups.some(rg => tokenMatchesGroup(token, rg)))
|
|
596
667
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
668
|
+
if (matched.length === 0) {
|
|
669
|
+
const validGroups = getValidGroupsList(allTokensWithGuidelines)
|
|
670
|
+
return {
|
|
671
|
+
content: [
|
|
672
|
+
{
|
|
673
|
+
type: 'text',
|
|
674
|
+
text: `No tokens found for groups: ${groups.join(', ')}.\n\n### Valid Groups:\n${validGroups}`,
|
|
675
|
+
},
|
|
676
|
+
],
|
|
677
|
+
}
|
|
678
|
+
}
|
|
602
679
|
|
|
603
|
-
|
|
604
|
-
|
|
680
|
+
let text = `Found ${matched.length} token(s) across ${resolvedGroups.length} group(s):\n\n${formatBundle(matched)}`
|
|
681
|
+
|
|
682
|
+
const activeHints = resolvedGroups.map(g => groupHints[g]).filter(Boolean)
|
|
683
|
+
|
|
684
|
+
if (activeHints.length > 0) {
|
|
685
|
+
text += `\n\n### ⚠️ Usage Guidance:\n${activeHints.map(h => `- ${h}`).join('\n')}`
|
|
686
|
+
}
|
|
605
687
|
|
|
606
688
|
return {
|
|
607
|
-
content: [
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
689
|
+
content: [{type: 'text', text}],
|
|
690
|
+
}
|
|
691
|
+
},
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
server.registerTool(
|
|
695
|
+
'get_design_token_specs',
|
|
696
|
+
{
|
|
697
|
+
description:
|
|
698
|
+
'CRITICAL: CALL THIS FIRST. Provides the logic matrix and the list of valid group names. You cannot search accurately without this map.',
|
|
699
|
+
annotations: {readOnlyHint: true},
|
|
700
|
+
},
|
|
701
|
+
async () => {
|
|
702
|
+
const groups = listTokenGroups()
|
|
703
|
+
const customRules = getDesignTokenSpecsText(groups)
|
|
704
|
+
let text: string
|
|
705
|
+
try {
|
|
706
|
+
const upstreamGuide = loadDesignTokensGuide()
|
|
707
|
+
text = `${customRules}\n\n---\n\n${upstreamGuide}`
|
|
708
|
+
} catch {
|
|
709
|
+
text = customRules
|
|
710
|
+
}
|
|
611
711
|
|
|
612
|
-
|
|
613
|
-
|
|
712
|
+
return {
|
|
713
|
+
content: [{type: 'text', text}],
|
|
714
|
+
}
|
|
715
|
+
},
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
server.registerTool(
|
|
719
|
+
'get_token_usage_patterns',
|
|
720
|
+
{
|
|
721
|
+
description:
|
|
722
|
+
'Provides "Golden Example" CSS for core patterns: Button (Interactions) and Stack (Layout). Use this to understand how to apply the Logic Matrix, Motion, and Spacing scales.',
|
|
723
|
+
annotations: {readOnlyHint: true},
|
|
724
|
+
},
|
|
725
|
+
async () => {
|
|
726
|
+
const customPatterns = getTokenUsagePatternsText()
|
|
727
|
+
let text: string
|
|
728
|
+
try {
|
|
729
|
+
const guide = loadDesignTokensGuide()
|
|
730
|
+
const goldenExampleMatch = guide.match(/## Golden Example[\s\S]*?(?=\n## |$)/)
|
|
731
|
+
if (goldenExampleMatch) {
|
|
732
|
+
text = `${customPatterns}\n\n---\n\n${goldenExampleMatch[0].trim()}`
|
|
733
|
+
} else {
|
|
734
|
+
text = customPatterns
|
|
735
|
+
}
|
|
736
|
+
} catch {
|
|
737
|
+
text = customPatterns
|
|
738
|
+
}
|
|
614
739
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
2. **Property Mismatch**: Do not search for CSS properties like "offset", "padding", or "font-size". Use semantic intent keywords: "danger", "muted", "emphasis".
|
|
618
|
-
3. **Typography**: Remember that \`caption\`, \`display\`, and \`code\` groups do NOT support size suffixes. Use the base shorthand only.
|
|
619
|
-
4. **Group Intent**: Use the \`group\` parameter instead of putting group names in the \`query\` string (e.g., use group: "stack" instead of query: "stack padding").`,
|
|
620
|
-
},
|
|
621
|
-
],
|
|
740
|
+
return {
|
|
741
|
+
content: [{type: 'text', text}],
|
|
622
742
|
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
const limitedResults = results.slice(0, limit)
|
|
626
|
-
|
|
627
|
-
let output: string
|
|
628
|
-
|
|
629
|
-
if (!query) {
|
|
630
|
-
output = `Found ${results.length} token(s). Showing top ${limitedResults.length}:\n\n`
|
|
631
|
-
} else {
|
|
632
|
-
output = `Found ${results.length} token(s) matching "${query}". Showing top ${limitedResults.length}:\n\n`
|
|
633
|
-
}
|
|
634
|
-
output += formatBundle(limitedResults)
|
|
635
|
-
|
|
636
|
-
if (results.length > limit) {
|
|
637
|
-
output += `\n\n*...and ${results.length - limit} more matches. Use more specific keywords to narrow the search.*`
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
return {
|
|
641
|
-
content: [{type: 'text', text: output}],
|
|
642
|
-
}
|
|
643
|
-
},
|
|
644
|
-
)
|
|
645
|
-
|
|
646
|
-
server.registerTool(
|
|
647
|
-
'get_token_group_bundle',
|
|
648
|
-
{
|
|
649
|
-
description:
|
|
650
|
-
"PREFERRED FOR COMPONENTS. Fetch all tokens for complex UI (e.g., Dialogs, Cards) in one call by providing an array of groups like ['overlay', 'shadow']. Use this instead of multiple find_tokens calls to save context.",
|
|
651
|
-
inputSchema: {
|
|
652
|
-
groups: z.array(z.string()).describe('Array of group names (e.g., ["overlay", "shadow", "focus"])'),
|
|
653
743
|
},
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
server.registerTool(
|
|
747
|
+
'lint_css',
|
|
748
|
+
{
|
|
749
|
+
description:
|
|
750
|
+
'REQUIRED FINAL STEP. Use this to validate your CSS. You cannot complete a task involving CSS without a successful run of this tool.',
|
|
751
|
+
inputSchema: z.object({css: z.string()}),
|
|
752
|
+
annotations: {readOnlyHint: true},
|
|
753
|
+
},
|
|
754
|
+
async ({css}) => {
|
|
755
|
+
try {
|
|
756
|
+
// --fix flag tells Stylelint to repair what it can
|
|
757
|
+
const {stdout} = await runStylelint(css)
|
|
758
|
+
|
|
759
|
+
return {
|
|
760
|
+
content: [
|
|
761
|
+
{
|
|
762
|
+
type: 'text',
|
|
763
|
+
text: stdout || '✅ Stylelint passed (or was successfully autofixed).',
|
|
764
|
+
},
|
|
765
|
+
],
|
|
766
|
+
}
|
|
767
|
+
} catch (error: unknown) {
|
|
768
|
+
// If Stylelint still has errors it CANNOT fix, it will land here
|
|
769
|
+
const errorOutput =
|
|
770
|
+
error instanceof Error && 'stdout' in error ? (error as Error & {stdout: string}).stdout : String(error)
|
|
771
|
+
return {
|
|
772
|
+
content: [
|
|
773
|
+
{
|
|
774
|
+
type: 'text',
|
|
775
|
+
text: `❌ Errors without autofix remaining:\n${errorOutput}`,
|
|
776
|
+
},
|
|
777
|
+
],
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
},
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
// -----------------------------------------------------------------------------
|
|
784
|
+
// Foundations
|
|
785
|
+
// -----------------------------------------------------------------------------
|
|
786
|
+
server.registerTool(
|
|
787
|
+
'get_color_usage',
|
|
788
|
+
{description: 'Get the guidelines for how to apply color to a user interface', annotations: {readOnlyHint: true}},
|
|
789
|
+
async () => {
|
|
790
|
+
const url = new URL(`/product/getting-started/foundations/color-usage`, 'https://primer.style')
|
|
791
|
+
const response = await fetch(url)
|
|
792
|
+
if (!response.ok) {
|
|
793
|
+
throw new Error(`Failed to fetch ${url} - ${response.statusText}`)
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const html = await response.text()
|
|
797
|
+
if (!html) {
|
|
798
|
+
return {
|
|
799
|
+
content: [],
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const $ = cheerio.load(html)
|
|
804
|
+
const source = $('main').html()
|
|
805
|
+
if (!source) {
|
|
806
|
+
return {
|
|
807
|
+
content: [],
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const text = turndownService.turndown(source)
|
|
812
|
+
|
|
668
813
|
return {
|
|
669
814
|
content: [
|
|
670
815
|
{
|
|
671
816
|
type: 'text',
|
|
672
|
-
text: `
|
|
817
|
+
text: `Here is the documentation for color usage in Primer:\n\n${text}`,
|
|
673
818
|
},
|
|
674
819
|
],
|
|
675
820
|
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
annotations: {readOnlyHint: true},
|
|
698
|
-
},
|
|
699
|
-
async () => {
|
|
700
|
-
const groups = listTokenGroups()
|
|
701
|
-
const customRules = getDesignTokenSpecsText(groups)
|
|
702
|
-
let text: string
|
|
703
|
-
try {
|
|
704
|
-
const upstreamGuide = loadDesignTokensGuide()
|
|
705
|
-
text = `${customRules}\n\n---\n\n${upstreamGuide}`
|
|
706
|
-
} catch {
|
|
707
|
-
text = customRules
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
return {
|
|
711
|
-
content: [{type: 'text', text}],
|
|
712
|
-
}
|
|
713
|
-
},
|
|
714
|
-
)
|
|
715
|
-
|
|
716
|
-
server.registerTool(
|
|
717
|
-
'get_token_usage_patterns',
|
|
718
|
-
{
|
|
719
|
-
description:
|
|
720
|
-
'Provides "Golden Example" CSS for core patterns: Button (Interactions) and Stack (Layout). Use this to understand how to apply the Logic Matrix, Motion, and Spacing scales.',
|
|
721
|
-
annotations: {readOnlyHint: true},
|
|
722
|
-
},
|
|
723
|
-
async () => {
|
|
724
|
-
const customPatterns = getTokenUsagePatternsText()
|
|
725
|
-
let text: string
|
|
726
|
-
try {
|
|
727
|
-
const guide = loadDesignTokensGuide()
|
|
728
|
-
const goldenExampleMatch = guide.match(/## Golden Example[\s\S]*?(?=\n## |$)/)
|
|
729
|
-
if (goldenExampleMatch) {
|
|
730
|
-
text = `${customPatterns}\n\n---\n\n${goldenExampleMatch[0].trim()}`
|
|
731
|
-
} else {
|
|
732
|
-
text = customPatterns
|
|
821
|
+
},
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
server.registerTool(
|
|
825
|
+
'get_typography_usage',
|
|
826
|
+
{
|
|
827
|
+
description: 'Get the guidelines for how to apply typography to a user interface',
|
|
828
|
+
annotations: {readOnlyHint: true},
|
|
829
|
+
},
|
|
830
|
+
async () => {
|
|
831
|
+
const url = new URL(`/product/getting-started/foundations/typography`, 'https://primer.style')
|
|
832
|
+
const response = await fetch(url)
|
|
833
|
+
if (!response.ok) {
|
|
834
|
+
throw new Error(`Failed to fetch ${url} - ${response.statusText}`)
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const html = await response.text()
|
|
838
|
+
if (!html) {
|
|
839
|
+
return {
|
|
840
|
+
content: [],
|
|
841
|
+
}
|
|
733
842
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
server.registerTool(
|
|
745
|
-
'lint_css',
|
|
746
|
-
{
|
|
747
|
-
description:
|
|
748
|
-
'REQUIRED FINAL STEP. Use this to validate your CSS. You cannot complete a task involving CSS without a successful run of this tool.',
|
|
749
|
-
inputSchema: {css: z.string()},
|
|
750
|
-
annotations: {readOnlyHint: true},
|
|
751
|
-
},
|
|
752
|
-
async ({css}) => {
|
|
753
|
-
try {
|
|
754
|
-
// --fix flag tells Stylelint to repair what it can
|
|
755
|
-
const {stdout} = await runStylelint(css)
|
|
843
|
+
|
|
844
|
+
const $ = cheerio.load(html)
|
|
845
|
+
const source = $('main').html()
|
|
846
|
+
if (!source) {
|
|
847
|
+
return {
|
|
848
|
+
content: [],
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const text = turndownService.turndown(source)
|
|
756
853
|
|
|
757
854
|
return {
|
|
758
855
|
content: [
|
|
759
856
|
{
|
|
760
857
|
type: 'text',
|
|
761
|
-
text:
|
|
858
|
+
text: `Here is the documentation for typography usage in Primer:\n\n${text}`,
|
|
762
859
|
},
|
|
763
860
|
],
|
|
764
861
|
}
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
862
|
+
},
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
// -----------------------------------------------------------------------------
|
|
866
|
+
// Icons
|
|
867
|
+
// -----------------------------------------------------------------------------
|
|
868
|
+
server.registerTool(
|
|
869
|
+
'list_icons',
|
|
870
|
+
{
|
|
871
|
+
description: 'List all of the icons (octicons) available from Primer Octicons React',
|
|
872
|
+
annotations: {readOnlyHint: true},
|
|
873
|
+
},
|
|
874
|
+
async () => {
|
|
875
|
+
const icons = listIcons().map(icon => {
|
|
876
|
+
const keywords = icon.keywords.map(keyword => {
|
|
877
|
+
return `<keyword>${keyword}</keyword>`
|
|
878
|
+
})
|
|
879
|
+
const sizes = icon.heights.map(height => {
|
|
880
|
+
return `<size value="${height}"></size>`
|
|
881
|
+
})
|
|
882
|
+
return [`<icon name="${icon.name}">`, ...keywords, ...sizes, `</icon>`].join('\n')
|
|
883
|
+
})
|
|
884
|
+
|
|
769
885
|
return {
|
|
770
886
|
content: [
|
|
771
887
|
{
|
|
772
888
|
type: 'text',
|
|
773
|
-
text:
|
|
889
|
+
text: `The following icons are available in the @primer/octicons-react package in TypeScript projects:
|
|
890
|
+
|
|
891
|
+
${icons.join('\n')}
|
|
892
|
+
|
|
893
|
+
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.`,
|
|
774
894
|
},
|
|
775
895
|
],
|
|
776
896
|
}
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
897
|
+
},
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
server.registerTool(
|
|
901
|
+
'get_icon',
|
|
902
|
+
{
|
|
903
|
+
description: 'Get a specific icon (octicon) by name from Primer',
|
|
904
|
+
inputSchema: z.object({
|
|
905
|
+
name: z.string().describe('The name of the icon to retrieve'),
|
|
906
|
+
size: z.string().optional().describe('The size of the icon to retrieve, e.g. "16"').default('16'),
|
|
907
|
+
}),
|
|
908
|
+
annotations: {readOnlyHint: true},
|
|
909
|
+
},
|
|
910
|
+
async ({name, size}) => {
|
|
911
|
+
const icons = listIcons()
|
|
912
|
+
const match = icons.find(icon => {
|
|
913
|
+
return icon.name === name || icon.name.toLowerCase() === name.toLowerCase()
|
|
914
|
+
})
|
|
915
|
+
if (!match) {
|
|
916
|
+
return {
|
|
917
|
+
content: [
|
|
918
|
+
{
|
|
919
|
+
type: 'text',
|
|
920
|
+
text: `There is no icon named \`${name}\` in the @primer/octicons-react package. For a full list of icons, use the \`get_icon\` tool.`,
|
|
921
|
+
},
|
|
922
|
+
],
|
|
923
|
+
}
|
|
798
924
|
}
|
|
799
|
-
}
|
|
800
925
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
content: [],
|
|
926
|
+
const url = new URL(`/octicons/icon/${match.name}-${size}`, 'https://primer.style')
|
|
927
|
+
const response = await fetch(url)
|
|
928
|
+
if (!response.ok) {
|
|
929
|
+
throw new Error(`Failed to fetch ${url}: ${response.statusText}`)
|
|
806
930
|
}
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
const text = turndownService.turndown(source)
|
|
810
|
-
|
|
811
|
-
return {
|
|
812
|
-
content: [
|
|
813
|
-
{
|
|
814
|
-
type: 'text',
|
|
815
|
-
text: `Here is the documentation for color usage in Primer:\n\n${text}`,
|
|
816
|
-
},
|
|
817
|
-
],
|
|
818
|
-
}
|
|
819
|
-
},
|
|
820
|
-
)
|
|
821
|
-
|
|
822
|
-
server.registerTool(
|
|
823
|
-
'get_typography_usage',
|
|
824
|
-
{
|
|
825
|
-
description: 'Get the guidelines for how to apply typography to a user interface',
|
|
826
|
-
annotations: {readOnlyHint: true},
|
|
827
|
-
},
|
|
828
|
-
async () => {
|
|
829
|
-
const url = new URL(`/product/getting-started/foundations/typography`, 'https://primer.style')
|
|
830
|
-
const response = await fetch(url)
|
|
831
|
-
if (!response.ok) {
|
|
832
|
-
throw new Error(`Failed to fetch ${url} - ${response.statusText}`)
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
const html = await response.text()
|
|
836
|
-
if (!html) {
|
|
837
|
-
return {
|
|
838
|
-
content: [],
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
931
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
932
|
+
const html = await response.text()
|
|
933
|
+
if (!html) {
|
|
934
|
+
return {
|
|
935
|
+
content: [],
|
|
936
|
+
}
|
|
847
937
|
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
const text = turndownService.turndown(source)
|
|
851
|
-
|
|
852
|
-
return {
|
|
853
|
-
content: [
|
|
854
|
-
{
|
|
855
|
-
type: 'text',
|
|
856
|
-
text: `Here is the documentation for typography usage in Primer:\n\n${text}`,
|
|
857
|
-
},
|
|
858
|
-
],
|
|
859
|
-
}
|
|
860
|
-
},
|
|
861
|
-
)
|
|
862
|
-
|
|
863
|
-
// -----------------------------------------------------------------------------
|
|
864
|
-
// Icons
|
|
865
|
-
// -----------------------------------------------------------------------------
|
|
866
|
-
server.registerTool(
|
|
867
|
-
'list_icons',
|
|
868
|
-
{
|
|
869
|
-
description: 'List all of the icons (octicons) available from Primer Octicons React',
|
|
870
|
-
annotations: {readOnlyHint: true},
|
|
871
|
-
},
|
|
872
|
-
async () => {
|
|
873
|
-
const icons = listIcons().map(icon => {
|
|
874
|
-
const keywords = icon.keywords.map(keyword => {
|
|
875
|
-
return `<keyword>${keyword}</keyword>`
|
|
876
|
-
})
|
|
877
|
-
const sizes = icon.heights.map(height => {
|
|
878
|
-
return `<size value="${height}"></size>`
|
|
879
|
-
})
|
|
880
|
-
return [`<icon name="${icon.name}">`, ...keywords, ...sizes, `</icon>`].join('\n')
|
|
881
|
-
})
|
|
882
938
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
939
|
+
const $ = cheerio.load(html)
|
|
940
|
+
const source = $('main').html()
|
|
941
|
+
if (!source) {
|
|
942
|
+
return {
|
|
943
|
+
content: [],
|
|
944
|
+
}
|
|
945
|
+
}
|
|
888
946
|
|
|
889
|
-
|
|
947
|
+
const text = turndownService.turndown(source)
|
|
890
948
|
|
|
891
|
-
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.`,
|
|
892
|
-
},
|
|
893
|
-
],
|
|
894
|
-
}
|
|
895
|
-
},
|
|
896
|
-
)
|
|
897
|
-
|
|
898
|
-
server.registerTool(
|
|
899
|
-
'get_icon',
|
|
900
|
-
{
|
|
901
|
-
description: 'Get a specific icon (octicon) by name from Primer',
|
|
902
|
-
inputSchema: {
|
|
903
|
-
name: z.string().describe('The name of the icon to retrieve'),
|
|
904
|
-
size: z.string().optional().describe('The size of the icon to retrieve, e.g. "16"').default('16'),
|
|
905
|
-
},
|
|
906
|
-
annotations: {readOnlyHint: true},
|
|
907
|
-
},
|
|
908
|
-
async ({name, size}) => {
|
|
909
|
-
const icons = listIcons()
|
|
910
|
-
const match = icons.find(icon => {
|
|
911
|
-
return icon.name === name || icon.name.toLowerCase() === name.toLowerCase()
|
|
912
|
-
})
|
|
913
|
-
if (!match) {
|
|
914
949
|
return {
|
|
915
950
|
content: [
|
|
916
951
|
{
|
|
917
952
|
type: 'text',
|
|
918
|
-
text: `
|
|
953
|
+
text: `Here is the documentation for the \`${name}\` icon at size: \`${size}\`:
|
|
954
|
+
${text}`,
|
|
919
955
|
},
|
|
920
956
|
],
|
|
921
957
|
}
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
const $ = cheerio.load(html)
|
|
938
|
-
const source = $('main').html()
|
|
939
|
-
if (!source) {
|
|
958
|
+
},
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
// -----------------------------------------------------------------------------
|
|
962
|
+
// Coding guidelines
|
|
963
|
+
// -----------------------------------------------------------------------------
|
|
964
|
+
server.registerTool(
|
|
965
|
+
'primer_coding_guidelines',
|
|
966
|
+
{
|
|
967
|
+
description: 'Get the guidelines when writing code that uses Primer or for UI code that you are creating',
|
|
968
|
+
annotations: {readOnlyHint: true},
|
|
969
|
+
},
|
|
970
|
+
async () => {
|
|
940
971
|
return {
|
|
941
|
-
content: [
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
const text = turndownService.turndown(source)
|
|
946
|
-
|
|
947
|
-
return {
|
|
948
|
-
content: [
|
|
949
|
-
{
|
|
950
|
-
type: 'text',
|
|
951
|
-
text: `Here is the documentation for the \`${name}\` icon at size: \`${size}\`:
|
|
952
|
-
${text}`,
|
|
953
|
-
},
|
|
954
|
-
],
|
|
955
|
-
}
|
|
956
|
-
},
|
|
957
|
-
)
|
|
958
|
-
|
|
959
|
-
// -----------------------------------------------------------------------------
|
|
960
|
-
// Coding guidelines
|
|
961
|
-
// -----------------------------------------------------------------------------
|
|
962
|
-
server.registerTool(
|
|
963
|
-
'primer_coding_guidelines',
|
|
964
|
-
{
|
|
965
|
-
description: 'Get the guidelines when writing code that uses Primer or for UI code that you are creating',
|
|
966
|
-
annotations: {readOnlyHint: true},
|
|
967
|
-
},
|
|
968
|
-
async () => {
|
|
969
|
-
return {
|
|
970
|
-
content: [
|
|
971
|
-
{
|
|
972
|
-
type: 'text',
|
|
973
|
-
text: `When writing code that uses Primer, follow these guidelines:
|
|
972
|
+
content: [
|
|
973
|
+
{
|
|
974
|
+
type: 'text',
|
|
975
|
+
text: `When writing code that uses Primer, follow these guidelines:
|
|
974
976
|
|
|
975
977
|
## Design Tokens
|
|
976
978
|
|
|
@@ -992,64 +994,82 @@ The following list of coding guidelines must be followed:
|
|
|
992
994
|
- Do not use the sx prop for styling components. Instead, use CSS Modules.
|
|
993
995
|
- Do not use the Box component for styling components. Instead, use CSS Modules.
|
|
994
996
|
`,
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
)
|
|
1000
|
-
|
|
1001
|
-
// -----------------------------------------------------------------------------
|
|
1002
|
-
// Accessibility
|
|
1003
|
-
// -----------------------------------------------------------------------------
|
|
1004
|
-
|
|
1005
|
-
/**
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
server.registerTool(
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
997
|
+
},
|
|
998
|
+
],
|
|
999
|
+
}
|
|
1000
|
+
},
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
// -----------------------------------------------------------------------------
|
|
1004
|
+
// Accessibility
|
|
1005
|
+
// -----------------------------------------------------------------------------
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* The `review_alt_text` tool is experimental and may be removed in future versions.
|
|
1009
|
+
*
|
|
1010
|
+
* The intent of this tool is to assist products like Copilot Code Review and Copilot Coding Agent
|
|
1011
|
+
* in reviewing both user- and AI-generated alt text for images, ensuring compliance with accessibility guidelines.
|
|
1012
|
+
* This tool is not intended to replace human-generated alt text; rather, it supports the review process
|
|
1013
|
+
* by providing suggestions for improvement. It should be used alongside human review, not as a substitute.
|
|
1014
|
+
*
|
|
1015
|
+
*
|
|
1016
|
+
**/
|
|
1017
|
+
server.registerTool(
|
|
1018
|
+
'review_alt_text',
|
|
1019
|
+
{
|
|
1020
|
+
description: 'Evaluates image alt text against accessibility best practices and context relevance.',
|
|
1021
|
+
inputSchema: z.object({
|
|
1022
|
+
surroundingText: z.string().describe('Text surrounding the image, relevant to the image.'),
|
|
1023
|
+
alt: z.string().describe('The alt text of the image being evaluated'),
|
|
1024
|
+
image: z.string().describe('The image URL or file path being evaluated'),
|
|
1025
|
+
}),
|
|
1026
|
+
annotations: {readOnlyHint: true},
|
|
1023
1027
|
},
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1028
|
+
async ({surroundingText, alt, image}, context) => {
|
|
1029
|
+
const response = inputResponse(context.mcpReq.inputResponses, 'altTextEvaluation')
|
|
1030
|
+
if (response.kind !== 'sampling') {
|
|
1031
|
+
return inputRequired({
|
|
1032
|
+
inputRequests: {
|
|
1033
|
+
altTextEvaluation: inputRequired.createMessage({
|
|
1034
|
+
messages: [
|
|
1035
|
+
{
|
|
1036
|
+
role: 'user',
|
|
1037
|
+
content: {
|
|
1038
|
+
type: 'text',
|
|
1039
|
+
text: `Does this alt text: '${alt}' meet accessibility guidelines and describe the image: ${image} accurately in context of this surrounding text: '${surroundingText}'?\n\n`,
|
|
1040
|
+
},
|
|
1041
|
+
},
|
|
1042
|
+
],
|
|
1043
|
+
temperature: 0.4,
|
|
1044
|
+
maxTokens: 500,
|
|
1045
|
+
}),
|
|
1046
|
+
},
|
|
1047
|
+
})
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
const content = Array.isArray(response.result.content) ? response.result.content : [response.result.content]
|
|
1051
|
+
const evaluation =
|
|
1052
|
+
content
|
|
1053
|
+
.filter(block => block.type === 'text')
|
|
1054
|
+
.map(block => block.text)
|
|
1055
|
+
.join('\n') || 'Unable to generate summary'
|
|
1056
|
+
|
|
1057
|
+
return {
|
|
1058
|
+
content: [
|
|
1059
|
+
{
|
|
1033
1060
|
type: 'text',
|
|
1034
|
-
text:
|
|
1061
|
+
text: evaluation,
|
|
1035
1062
|
},
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
altTextEvaluation: response.content.type === 'text' ? response.content.text : 'Unable to generate summary',
|
|
1050
|
-
nextSteps: `If the evaluation indicates issues with the alt text, provide more meaningful alt text based on the feedback. DO NOT run this tool repeatedly on the same image - evaluations may vary slightly with each run.`,
|
|
1051
|
-
}
|
|
1052
|
-
},
|
|
1053
|
-
)
|
|
1054
|
-
|
|
1055
|
-
export {server}
|
|
1063
|
+
],
|
|
1064
|
+
altTextEvaluation: evaluation,
|
|
1065
|
+
nextSteps: `If the evaluation indicates issues with the alt text, provide more meaningful alt text based on the feedback. DO NOT run this tool repeatedly on the same image - evaluations may vary slightly with each run.`,
|
|
1066
|
+
}
|
|
1067
|
+
},
|
|
1068
|
+
)
|
|
1069
|
+
|
|
1070
|
+
return server
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const server = createServer()
|
|
1074
|
+
|
|
1075
|
+
export {createServer, server}
|