diff --git a/.changeset/mcp-scenario-pattern-llms.md b/.changeset/mcp-scenario-pattern-llms.md new file mode 100644 index 00000000000..eb848b6da7a --- /dev/null +++ b/.changeset/mcp-scenario-pattern-llms.md @@ -0,0 +1,5 @@ +--- +'@primer/mcp': minor +--- + +Load scenario pattern listings and guidance from Primer's dedicated LLM endpoints. diff --git a/packages/mcp/src/server.test.ts b/packages/mcp/src/server.test.ts index a20e6a97429..b73be6a25bb 100644 --- a/packages/mcp/src/server.test.ts +++ b/packages/mcp/src/server.test.ts @@ -162,6 +162,34 @@ describe('MCP server', () => { expect(vi.getTimerCount()).toBe(0) }) + it('lists scenario patterns from the scenario patterns llms.txt endpoint', async () => { + const fetchMock = vi.mocked(fetch) + fetchMock.mockResolvedValue(new Response('# Scenario patterns\n\n- [Copy](./copy)')) + + const result = await client.callTool({name: 'list_patterns'}) + + expect(fetchMock).toHaveBeenCalledWith(new URL('https://primer.style/product/scenario-patterns/llms.txt')) + expect(result.content).toContainEqual( + expect.objectContaining({ + type: 'text', + text: expect.stringContaining('# Scenario patterns\n\n- [Copy](./copy)'), + }), + ) + }) + + it('gets scenario patterns from their llms.txt endpoints', async () => { + const fetchMock = vi.mocked(fetch) + fetchMock.mockResolvedValue(new Response('# Copy\n\nCopy guidance')) + + const result = await client.callTool({ + name: 'get_pattern', + arguments: {name: 'Copy'}, + }) + + expect(fetchMock).toHaveBeenCalledWith(new URL('https://primer.style/product/scenario-patterns/copy/llms.txt')) + expect(result.content).toEqual([{type: 'text', text: '# Copy\n\nCopy guidance'}]) + }) + it('reviews alt text through the multi-round-trip sampling flow', async () => { const result = await client.callTool({ name: 'review_alt_text', diff --git a/packages/mcp/src/server.ts b/packages/mcp/src/server.ts index f1e031a27ed..be1de743258 100644 --- a/packages/mcp/src/server.ts +++ b/packages/mcp/src/server.ts @@ -441,13 +441,19 @@ ${text}`, 'list_patterns', { description: - '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.', + 'List all of the patterns available from Primer React. Scenario patterns describe specific user tasks. Prefer a scenario pattern when one fits the task, and fall back to the more generic UI patterns otherwise.', annotations: {readOnlyHint: true}, }, async () => { const all = listPatterns() - const scenario = all.filter(pattern => pattern.category === 'scenario').map(pattern => `- ${pattern.name}`) const ui = all.filter(pattern => pattern.category === 'ui').map(pattern => `- ${pattern.name}`) + const url = new URL('/product/scenario-patterns/llms.txt', 'https://primer.style') + const response = await fetch(url) + if (!response.ok) { + throw new Error(`Failed to fetch ${url} - ${response.statusText}`) + } + + const scenario = await response.text() return { content: [ { @@ -456,7 +462,7 @@ ${text}`, ## Scenario patterns -${scenario.join('\n')} +${scenario} ## UI patterns @@ -471,7 +477,7 @@ ${ui.join('\n')}`, 'get_pattern', { description: - '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.', + 'Get a specific pattern by name. Scenario patterns describe specific user tasks. Prefer a scenario pattern when one fits the task, and fall back to the more generic UI patterns otherwise.', inputSchema: z.object({ name: z.string().describe('The name of the pattern to retrieve'), }), @@ -495,20 +501,32 @@ ${ui.join('\n')}`, } const basePath = match.category === 'scenario' ? 'scenario-patterns' : 'ui-patterns' - const url = new URL(`/product/${basePath}/${match.id}`, 'https://primer.style') + const suffix = match.category === 'scenario' ? '/llms.txt' : '' + const url = new URL(`/product/${basePath}/${match.id}${suffix}`, 'https://primer.style') const response = await fetch(url) if (!response.ok) { throw new Error(`Failed to fetch ${url} - ${response.statusText}`) } - const html = await response.text() - if (!html) { + const body = await response.text() + if (!body) { return { content: [], } } - const $ = cheerio.load(html) + if (match.category === 'scenario') { + return { + content: [ + { + type: 'text', + text: body, + }, + ], + } + } + + const $ = cheerio.load(body) const source = $('main').html() if (!source) { return {