Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-component-text-sizes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/brand-mcp': patch
---

Updated `primer_brand_page_design` tool to recommend component text defaults and `primer_brand_review` to warn about unnecessary `size` props.
8 changes: 8 additions & 0 deletions .changeset/connect-gridlines-to-page-edges.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@primer/brand-mcp': patch
---

Updated MCP gridline tools to prevent disconnected, centered border boxes:

- Updated `primer_brand_page_design` to prefer built-in gridline APIs and scope custom page-width frames to grouped Cards and Pillars.
- Updated `primer_brand_examples` to preserve canonical Card/Pillar companion CSS and surface the focused Statistic gridline example.
5 changes: 5 additions & 0 deletions .changeset/remove-card-skew-effect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react-brand': minor
---

⚠️ `CardSkewEffect` has been removed from `@primer/react-brand`. The effect has been retired, and no replacement will be issued.
9 changes: 9 additions & 0 deletions .changeset/route-agents-to-primer-brand.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@primer/brand-mcp': minor
---

Updated `primer_brand_setup` to create or update `AGENTS.md` beside a `package.json` that declares `@primer/react-brand`.

- Preserves existing instructions and appends/replaces only the Primer Brand instructions.
- Requires `projectDir` when several workspace packages declare Primer Brand.
- Routes future agents to the MCP tools and version-matched local documentation.
70 changes: 39 additions & 31 deletions apps/next-docs/content/components/Card/react.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -169,37 +169,45 @@ When an image should stretch to fill the card width, set `padding="none"` on `Ca

```jsx live
<Grid>
{[
{
heading: 'Build with GitHub Copilot',
description: 'Write, review, and understand code with AI-powered assistance.',
image: '/images/placeholder-1.png',
},
{
heading: 'Automate your workflow',
description: 'Keep projects moving with tools that fit the way your team works.',
image: '/images/placeholder-2.png',
},
{
heading: 'Ship secure software',
description: 'Find and fix vulnerabilities throughout the development lifecycle.',
image: '/images/placeholder-3.png',
},
].map(card => (
<Grid.Column key={card.heading} span={{xsmall: 12, large: 4}}>
<Card href="https://github.com" fullWidth hasBorder ctaVariant="arrow">
<Card.Heading>{card.heading}</Card.Heading>
<Card.Description>{card.description}</Card.Description>
<Card.Image
position="block-end"
padding="none"
src={card.image}
alt="Abstract gradient illustration"
aspectRatio="4:3"
/>
</Card>
</Grid.Column>
))}
<Grid.Column span={{xsmall: 12, large: 4}}>
<Card href="https://github.com" fullWidth hasBorder ctaVariant="arrow">
<Card.Heading>Build with GitHub Copilot</Card.Heading>
<Card.Description>Write, review, and understand code with AI-powered assistance.</Card.Description>
<Card.Image
position="block-end"
padding="none"
src="/images/placeholder-1.png"
alt="Abstract gradient illustration"
aspectRatio="4:3"
/>
</Card>
</Grid.Column>
<Grid.Column span={{xsmall: 12, large: 4}}>
<Card href="https://github.com" fullWidth hasBorder ctaVariant="arrow">
<Card.Heading>Automate your workflow</Card.Heading>
<Card.Description>Keep projects moving with tools that fit the way your team works.</Card.Description>
<Card.Image
position="block-end"
padding="none"
src="/images/placeholder-2.png"
alt="Abstract gradient illustration"
aspectRatio="4:3"
/>
</Card>
</Grid.Column>
<Grid.Column span={{xsmall: 12, large: 4}}>
<Card href="https://github.com" fullWidth hasBorder ctaVariant="arrow">
<Card.Heading>Ship secure software</Card.Heading>
<Card.Description>Find and fix vulnerabilities throughout the development lifecycle.</Card.Description>
<Card.Image
position="block-end"
padding="none"
src="/images/placeholder-3.png"
alt="Abstract gradient illustration"
aspectRatio="4:3"
/>
</Card>
</Grid.Column>
</Grid>
```

Expand Down
4 changes: 3 additions & 1 deletion apps/next-docs/content/introduction/mcp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The server exposes a set of tools that AI agents can call:

| Tool | What it does |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `primer_brand_setup` | Configures Primer Brand first-time. Includes `ThemeProvider`, Mona Sans and RSC-directive configuration among other things |
| `primer_brand_setup` | Configures Primer Brand first-time and maintains a Primer Brand pointer beside the declaring `package.json`. |
| `primer_brand_page_design` | Page-design conventions (header/footer, hero media, gridline aesthetic, card grids, labels) and the current-brand reference templates to start from. |
| `primer_brand_component` | Get a component API information like props, allowed values and named sub-components. |
| `primer_brand_examples` | Reference examples taken from our Storybook. |
Expand All @@ -42,6 +42,8 @@ Add the server to your workspace's `.vscode/mcp.json` (or your user `mcp.json`):
}
```

When an agent calls `primer_brand_setup`, the tool updates `AGENTS.md` only in a package whose `package.json` declares `@primer/react-brand`. Existing instructions are preserved. If several workspace packages declare the dependency, call setup with `projectDir` set to the intended workspace-relative package folder. The pointer routes future agents to the MCP tools and version-matched docs bundled with the installed package.

## Set up in other clients

For GitHub Copilot CLI and other `stdio`-enabled MCP clients, run the server directly:
Expand Down
4 changes: 3 additions & 1 deletion packages/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ It is a version-aware, `stdio` (local) server which reads the docs and metadata

| Tool | What it does |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `primer_brand_setup` | Configures Primer Brand first-time. Includes `ThemeProvider`, Mona Sans and RSC-directive configuration among other things |
| `primer_brand_setup` | Configures Primer Brand first-time and maintains a Primer Brand pointer beside the declaring `package.json`. |
| `primer_brand_page_design` | Page-design conventions (header/footer, hero media, gridline aesthetic, card grids, labels) and the current-brand reference templates to start from. |
| `primer_brand_component` | Get a component API information like props, allowed values and named sub-components. |
| `primer_brand_examples` | Reference examples taken from our Storybook. |
Expand All @@ -35,6 +35,8 @@ Requires Node.js >= 24.
}
```

When an agent calls `primer_brand_setup`, the tool updates `AGENTS.md` only in a package whose `package.json` declares `@primer/react-brand`. Existing instructions are preserved. If several workspace packages declare the dependency, call setup with `projectDir` set to the intended workspace-relative package folder. The pointer routes future agents to the MCP tools and version-matched docs bundled with the installed package.

### Copilot CLI / other stdio clients

Run the server with `npx @primer/brand-mcp@latest`.
Expand Down
20 changes: 15 additions & 5 deletions packages/mcp/content/page-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ Pull adaptable component snippets with `primer_brand_examples`.
`@primer/react-brand` spans multiple generations of GitHub's design language, and many components expose that choice as a variant or prop. **Gridline** is the current brand direction. It's a connected, ruled-line aesthetic, as seen throughout the Flexsuite templates. Bias toward it.

- **Do** — when a component offers a gridline-family option, choose it over the older or default look. "Expressive" gridline options are a more pronounced sub-variant of the same aesthetic, not a competing style; reach for them where a section wants extra emphasis.
- **Do** — use a component's built-in `gridline` variant, gridline-family variant, or `hasGridLines` prop when available.
- **Don't** — settle for a component's plainest or legacy variant, or borrow section layouts from an older brand generation, when a gridline equivalent exists.
- **Don't** — recreate built-in gridlines with custom `Box` wrappers, border props, or CSS.

The gridline look is crucial and literal, not just a set of variants. Gridlines are thin ruled lines that frame the content column and separate sections (see "Contain content within the grid" section below).

Expand All @@ -36,17 +38,22 @@ Every full page should be framed top and bottom, unless the user has requested a

### Contain content within the grid

Body content must sit in one centered, max-width grid framed by the brand's gridlines — it should rarely stretch edge-to-edge. Treat gridlines as one continuous page structure: every line endpoint must reach the viewport or shared page-grid boundary, or intersect another gridline. Never let a border stop in empty space.
Body content must sit in one centered, max-width grid framed by the brand's gridlines — it should rarely stretch edge-to-edge. Treat gridlines as one continuous page structure. A gridline group must not look like an isolated four-sided box floating inside the page gutters.

Examples of this are in the Flexsuite recipes.

- **Do** — keep heroes, `River`s, `ComparisonTable` / `PricingOptions`, connected Card or Pillar groups, forms, `CTABanner`s, and prose on the same `Grid` / `Section` column. Draw thin side rules on the column and full-bleed horizontal rules between major sections.
- **Do** — for connected panels, put block rules on the outer frame, outer inline rules on the centered content container, and internal dividers on zero-gap cell wrappers. Preserve these intersections when max-width or column count changes at responsive breakpoints.
- **Do** — give a panel frame's block-start and block-end rules the same extent. They may differ only when one rule is also a page-wide section boundary that intersects the frame's inline rules.
- **Do** — for connected `Card` and `Pillar` groups, put block-start and block-end rules on a page-width outer frame outside the centered, max-width `Grid` / `Section`. Put outer inline rules on the centered content container and internal dividers on zero-gap cell wrappers.
- **Do** — for these custom group frames, make every horizontal rule reach both viewport edges. It may instead end at vertical page-grid rules only when those rules continue beyond the group into adjacent sections.
- **Do** — preserve rule intersections when the max-width or column count changes at responsive breakpoints.

- **Don't** — put all four outer borders for a `Card` or `Pillar` group on a centered, max-width container. This creates a disconnected box with empty margins around it.
- **Don't** — assume `width: 100%` makes a custom group frame full bleed when it is inside a constrained parent. The outer frame must sit outside that parent or use the page's established full-bleed breakout.
- **Don't** — give sections different widths, add gaps between borders that should connect, or put borders on inset wrappers where their endpoints stop in empty space.
- **Don't** — stretch content edge-to-edge or let a section bleed full-width unless it is a deliberate background band behind the shared grid.

Retrieve the canonical grouped `Card` or `Pillar` structure with `primer_brand_examples` and carry its companion CSS with the JSX. Do not reproduce that structure from this prose.

- **Mobile** — use a modest, consistent side inset and make sure nothing overflows the viewport.

Resolve gutter, inset, and gridline (border) values with `primer_brand_tokens`; don't hardcode hex or pixel values.
Expand All @@ -63,7 +70,9 @@ Generous spacing is what lets a layout breathe; the Flexsuite recipes are a good

A page has one clear headline and a calm step-down from there.

- **Do** — use the default `size` of component-owned text such as `Card.Heading`, `Hero.Heading`, and `FAQ.Heading`. Set `size` on standalone `Heading` and `Text`.
- **Do** — use a single hero heading, make secondary section headings a clear step smaller, and keep body copy regular weight. Left-align long-form copy; reserve centering for short hero or section intros.
- **Don't** — pass `size` to a component-owned heading, description, label, or other text unless the brief requires an override. Changing `as` for semantic heading order does not require changing `size`.
- **Don't** — size secondary headings close to the hero, set body text in heavy weights, or center long paragraphs.

Resolve exact sizes and weights with `primer_brand_tokens`.
Expand Down Expand Up @@ -126,14 +135,15 @@ Resolve exact sizes and weights with `primer_brand_tokens`.

**Do**

- Default to placing `Card`, `Pillar`, `Box`, or custom items in a square frame using `<Grid columnGap="none" rowGap="none" enableGutters={false}>`.
- Default to placing connected `Card` or `Pillar` groups in a page-width outer frame around a centered zero-gap `Grid`. Retrieve the current structure and companion CSS with `primer_brand_examples`.
- When Card images need to stretch to fill the Card width, set `padding="none"` on every `Card.Image` and `hasBorder` on every `Card` to provide visual containment. Place these full-bleed Cards in a regular `Grid` with a clear gap between them; this is a separate visual treatment from gridline Cards. Keep image position and aspect ratio consistent across the group, and retrieve Card examples with `primer_brand_examples` for reference.
- Draw shared dividers on custom frame/cell wrappers and use `border-radius: 0` there.

**Don't**

- Put native-bordered, full-bleed Cards inside a shared gridline frame, or mix gridline and native-border treatments within one group.
- Render other repeated panels as separate rounded cards, double their shared borders, or override `Card` / `Pillar` internals.
- Apply the custom Card/Pillar frame pattern to components that already provide a gridline variant or prop.
- Box a repeated panel group inside the centered content width, render panels as separate rounded cards, double their shared borders, or override `Card` / `Pillar` internals.
- `Pillar` has no grid variant; don't invent one.

### CTABanner
Expand Down
10 changes: 6 additions & 4 deletions packages/mcp/scripts/generate-catalog.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ function bestStoryExample(directory, name, byName) {
const declarationBody = source.slice(exportMatches[index].index, exportMatches[index + 1]?.index ?? source.length)
for (const match of declarationBody.matchAll(/(?:=>|\breturn)\s*\(/g)) {
const capturedJsx = captureBalanced(declarationBody, match.index + match[0].length - 1, '(', ')')
if (capturedJsx && capturedJsx.includes('<')) snippets.push(capturedJsx)
if (capturedJsx && capturedJsx.includes('<')) snippets.push({capturedJsx, exportName: exportMatches[index][1]})
}
}
return snippets
Expand Down Expand Up @@ -299,9 +299,11 @@ function bestStoryExample(directory, name, byName) {
}

// Must render the component; sub-composition helps, plumbing/length hurt. -1 rejects.
const scoreSnippet = (code, fromExamplesFile) => {
const scoreSnippet = (code, fromExamplesFile, exportName) => {
if (!new RegExp(`<${name}(?:\\b|\\.)`).test(code)) return -1
let total = fromExamplesFile ? 3 : 0
// An exact `Gridline` story is the component's focused canonical composition, not a mixed showcase.
if (/^gridline$/i.test(exportName)) total += 10
total += (code.match(new RegExp(`<${name}\\.`, 'g')) || []).length
for (const pattern of EXAMPLE_NOISE) total -= (code.match(pattern) || []).length
if (code.length < 40) total -= 5
Expand All @@ -317,10 +319,10 @@ function bestStoryExample(directory, name, byName) {
const source = readFileOrNull(file)
if (!source) continue
const fromExamplesFile = /\.examples?\.stories\.tsx$/i.test(file)
for (const capturedJsx of jsxSnippets(source)) {
for (const {capturedJsx, exportName} of jsxSnippets(source)) {
const code = dedentJsx(capturedJsx)
if (code.length > 4000 || exampleContradictsCatalog(code, byName)) continue
const relevanceScore = scoreSnippet(code, fromExamplesFile)
const relevanceScore = scoreSnippet(code, fromExamplesFile, exportName)
if (relevanceScore >= 0 && (!bestExample || relevanceScore > bestExample.score))
bestExample = {code, styles: referencedStyles(file, source, code), score: relevanceScore}
}
Expand Down
28 changes: 26 additions & 2 deletions packages/mcp/scripts/smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,27 @@
*/
import {Client} from '@modelcontextprotocol/sdk/client/index.js'
import {StdioClientTransport} from '@modelcontextprotocol/sdk/client/stdio.js'
import {mkdtempSync, readFileSync, rmSync, writeFileSync} from 'node:fs'
import {tmpdir} from 'node:os'
import {dirname, join} from 'node:path'
import {fileURLToPath} from 'node:url'

function assert(condition, message) {
if (!condition) throw new Error(message)
}

const transport = new StdioClientTransport({command: 'node', args: ['dist/index.js']})
const packageDir = dirname(dirname(fileURLToPath(import.meta.url)))
const smokeProject = mkdtempSync(join(tmpdir(), 'primer-brand-mcp-smoke-'))
writeFileSync(
join(smokeProject, 'package.json'),
JSON.stringify({dependencies: {vite: '^7.0.0', '@primer/react-brand': '^0.73.0'}}),
)

const transport = new StdioClientTransport({
command: 'node',
args: [join(packageDir, 'dist', 'index.js')],
cwd: smokeProject,
})
const client = new Client({name: 'primer-brand-mcp-smoke', version: '0.0.0'})

try {
Expand All @@ -32,7 +47,15 @@ try {
return text
}

await call('primer_brand_setup', {framework: 'next-app'})
const setup = await call('primer_brand_setup', {framework: 'next-app'})
assert(
setup.includes('Created `AGENTS.md` beside the selected package.json'),
'primer_brand_setup did not report creating AGENTS.md',
)
assert(
readFileSync(join(smokeProject, 'AGENTS.md'), 'utf8').includes('<!-- BEGIN:primer-brand-mcp -->'),
'primer_brand_setup did not create the managed AGENTS.md block',
)
await call('primer_brand_page_design', {})
await call('primer_brand_component', {name: 'Hero'})
await call('primer_brand_examples', {goal: 'education landing page'})
Expand Down Expand Up @@ -60,4 +83,5 @@ try {
} catch {
// ignore errors while closing the client
}
rmSync(smokeProject, {recursive: true, force: true})
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {existsSync, readFileSync, realpathSync} from 'node:fs'
import {dirname, join, normalize, resolve, sep} from 'node:path'

import type {Logger} from '../logger.js'
import type {BrandInstall} from './resolve-install.js'
import type {Logger} from '../../logger.js'
import type {BrandInstall} from '../resolve-install/resolve-install.js'

/**
* One entry in the documentation table of contents (parsed from `llms.txt`).
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {existsSync, readdirSync, readFileSync} from 'node:fs'
import {dirname, join} from 'node:path'

import type {CatalogAsset} from '../catalog/types.js'
import type {CatalogAsset} from '../../catalog/types.js'

type AssetPackage = {
module: string
Expand Down
Loading
Loading