design-system-mcp: Add new package for design system MCP tooling#77159
Conversation
|
Size Change: 0 B Total Size: 7.75 MB ℹ️ View Unchanged
|
|
Flaky tests detected in e04a7ec. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/24669401242
|
| }, | ||
| "dependencies": { | ||
| "@cfworker/json-schema": "4.1.1", | ||
| "@modelcontextprotocol/server": "2.0.0-alpha.2", |
There was a problem hiding this comment.
Is it safe to be based on a package on an alpha release, which may introduce breaking changes until it stabilizes?
There was a problem hiding this comment.
Is it safe to be based on a package on an alpha release, which may introduce breaking changes until it stabilizes?
"safe" in what sense? The way we're abstracting this, we can absorb the cost of whatever breaking changes might happen. I recall making this choice intentionally, and it may have been to preempt the eventual breaking changes we'd have to address anyways with going from 1.x to 2.x (i.e. I'm hoping it's less work for us to manage an upgrade from 2.0.0-alpha to 2.0.0 than it is from 1.x to 2.0.0).
There was a problem hiding this comment.
It's also worth noting that I'm considering this package itself to be an early release and unstable. I clarified this in e04a7ec.
| const MANIFEST_URL = | ||
| 'https://wordpress.github.io/gutenberg/manifests/components.json'; | ||
| const TOKENS_URL = | ||
| 'https://raw.githubusercontent.com/WordPress/gutenberg/refs/heads/trunk/packages/theme/docs/tokens.md'; |
There was a problem hiding this comment.
Minor for an initial release, but Claude flags that the in-memory cache for both the component manifest and the design tokens document has no TTL or expiration. Since MCP servers are long-lived processes, this means the data is fetched once on first use and never refreshed until the server is restarted.
Could we add a simple TTL ?
There was a problem hiding this comment.
I wondered about this as well, though my understanding is that the MCP server process usually lives as long as the agent process itself, so e.g. when you exit and reopen Cursor or a claude terminal process. If that generally happens somewhere in the range of "multiple times per day" to "every couple days", then I don't know if it's worth the added complexity of managing cache lifecycle ourselves?
| "wordpress-design-system": { | ||
| "command": "npx", | ||
| "args": [ "-y", "@wordpress/design-system-mcp" ] | ||
| } |
There was a problem hiding this comment.
One thing I'm not sure about is whether we should pin this to @latest.
| "wordpress-design-system": { | |
| "command": "npx", | |
| "args": [ "-y", "@wordpress/design-system-mcp" ] | |
| } | |
| "wordpress-design-system": { | |
| "command": "npx", | |
| "args": [ "-y", "@wordpress/design-system-mcp@latest" ] | |
| } |
Pros:
- Always using the latest version
- Don't have to worry upgrade workflows or forever maintaining details here like URL location of tokens
Cons:
- It takes a bit more time to start up because it needs to check and maybe download the newest version
- Supply chain attacks are prevalent in the NPM space and I don't want to contribute to that risk (i.e. if a transitive dependency becomes compromised around the same time someone starts interacting with the MCP)
Prior art:
There was a problem hiding this comment.
In a82de43 and a0151a9 I opted for a combination of using @latest, but also providing some "hardening" arguments (--ignore-scripts and --min-release-age). This might be overkill, but I don't think it's really negatively affecting any setup workflows to include them. Note that --min-release-age is a very new NPM feature only available in v11.10.0+, but I tested and confirmed it's essentially a no-op in older versions. NPM v11.0.0-v11.9.0 will log a warning about an unrecognized config parameter, but it's harmless.
3fa948b to
a0151a9
Compare
|
@ciampo In a76be90 I added a plain |
| #!/usr/bin/env node | ||
|
|
||
| import { StdioServerTransport } from '@modelcontextprotocol/server'; | ||
| import { createServer } from '@wordpress/design-system-mcp'; |
There was a problem hiding this comment.
It's a little strange to reference the package's own name here like this, but the only other way I could think to address this was using ../build-module/index.mjs, which is equally/more strange. I tried ../ and .. but it errors because Node.js's ESM doesn't support directory imports (I was hoping it would resolve based on the package.json . exports, but it doesn't).
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
I'm marking this ready for review because, despite the components manifest having problems, it's a separate, pre-existing issue. The package proposed here will use whatever the latest manifest content is available, so we can address that fix separately. Those fixes are being explored in #77112 and #77382. |
mirka
left a comment
There was a problem hiding this comment.
All my comments are non-blocking if certain things are to be refined later after merge. But we should also be conscious that this MCP doesn't make things even more confusing for our consumers, if we are going to immediately start encouraging use.
| * `@wordpress/` npm namespace (not always accurate, see `packages/wp-build`). | ||
| */ | ||
| const PACKAGE_DIR_TO_NAME: Record< string, string > = { | ||
| ui: '@wordpress/ui', |
There was a problem hiding this comment.
In real usage, I assume we're going to have a mix of recommend components from @wordpress/components and @wordpress/ui components for a while. Shouldn't the MCP understand what the exact recommended component set is, from a source like the use-recommended-components lint? It would be a bad experience if the MCP recommended a component but a lint rule told you it's not allowed yet, or the MCP doesn't know about a component that exists in @wordpress/components but not yet in @wordpress/ui.
There was a problem hiding this comment.
In real usage, I assume we're going to have a mix of recommend components from
@wordpress/componentsand@wordpress/uicomponents for a while. Shouldn't the MCP understand what the exact recommended component set is
Yeah, I agree with this, and commented something similar at #77206 (comment) & #77264 (comment).
I'm on the fence whether we'd need that for this initial offering or we could explore that as a follow-on. Maybe I'll do a timeboxed excercrise to see how complex that might end up being.
There was a problem hiding this comment.
I think you're right that setting someone up to run into lint errors with the use-recommended-components rule is setting them up for failure, but I'd also be very sad if our MCP server tells people about the existance of only 3 components. Puts us in a tough position. Ideally we'd be allowlisting a lot more here, and I know we're rolling in that direction.
I guess the best thing would be to prefer in order:
- Allowlisted components
@wordpress/componentsthat are "stable" and not included in the denylist
Which ultimately means it's going to be majority @wordpress/components for now, which frankly matches what we'd expect, and at least provides a collection of components for people to work with, even if they're not the @wordpress/ui design system components.
There was a problem hiding this comment.
In real usage, I assume we're going to have a mix of recommend components from
@wordpress/componentsand@wordpress/uicomponents for a while.
This was the hardest one to get right, but also the most important I think.
Where I landed in 180d820 was to take advantage of Storybook manifest curation so that the only components that are included in the manifests are the ones that we recommend for use.
Originally I was hoping we could do this semi-automatically based on componentStatus. And technically we can. But in iterating on this with my friend Claude, the solution involves an experimental_indexers feature and traversing AST trees to get the meta componentStatus value. I saved the code if we want it, but it felt a bit too fragile for my liking.
Instead, I opted to just go the manual route, adding the tags metadata to the 59 components (61 stories) based on what exists today from a combination of (a) componentStatus.status === 'stable' && componentStatus.whereUsed === 'global' + (b) @wordpress/ui components already included in the use-recommended-components allowlist. This adds a bit of manual overhead on our part, but feels like the simplest option for the short-term.
There was a problem hiding this comment.
Sounds good. I wonder if there's a better name for the tag though, manifest is a bit vague if you don't know what it is ("manifest for what?").
There was a problem hiding this comment.
I agree, though with it being a built-in tag I don't think it's something we have much control over. I am curious to continue exploring a more automated approach here, or we could even adapt our approach to componentStatus to better serve this tooling. Ideally this is a short-term, very manual solution.
Though you also reminded me that it's also potential a strange abuse of manifests for containing certain types of components. Which I'm mostly okay with, because we're using the manifests explicitly for this one use-case of documenting design system components. I also don't think it's entirely wrong to include only general-purpose, recommended components in a manifest for our Storybook. An alternative here would be having separate Storybook site for the design system.
There was a problem hiding this comment.
Oh, didn't realize it was a built-in Storybook tag.
a0151a9 to
656732a
Compare
|
The testing flow from the original comment is still accurate, but worth noting that it's using the live manifest, which doesn't reflect the filtering we're expecting after 180d820 (and therefore includes a lot of components we wouldn't expect). To test this locally, I added environment variables in 6fe1ad9. The way I tested this locally:
Then the
|
656732a to
7092eec
Compare
mirka
left a comment
There was a problem hiding this comment.
The wrong import paths should probably be fixed before initial release, but otherwise looks good enough for the first iteration 👍
| name: canonicalName, | ||
| description: component.description || '', | ||
| packageName: pkg, | ||
| importStatement: `import { ${ canonicalName } } from '${ pkg }';`, |
There was a problem hiding this comment.
This can be wrong when the component needs to be imported with a __experimental prefix or as a private API.
Good catch on this. It's tricky to solve in a programmatic way, since we don't have access to the actual exported name. It doesn't appear anywhere in the manifest. And we can't assume that My leaning is that maybe we could have a manual mapping that lives in the MCP package for now. If we expect that eventually we'd be moving toward Right now, this affects 6 experimental components and 2 private components:
Maybe we shouldn't even be including those private components in the manifest in the first place. They're useful in a Gutenberg context, but not other projects. 🤔 |
|
Another option is to exclude all of those components from the manifest. I mean, they're experimental after all, right? |
Helps testing in local development
085dc0e to
ebbba42
Compare
) * design-system-mcp: Add new package for design system MCP tooling * Create standalone executable bin script for starting server * Pin npx command to latest version of package * Harden npx command flags * Use raw type property value if available * Derive import statement from name and package * Use top-level component name as canonical import * Collapse and concatenate story sets for same component * Include only stable, global components in manifest * Support env variable overrides for remote URLs Helps testing in local development * Sort components by name * Use non-empty descriptions/props even if discovered later * Remove private components from manifest * Add alias mapping for experimental components * Add experimental notice to MCP package Co-authored-by: aduth <aduth@git.wordpress.org> Co-authored-by: ciampo <mciampini@git.wordpress.org> Co-authored-by: mirka <0mirka00@git.wordpress.org>




What?
Closes #77206
Implements a new package
@wordpress/design-system-mcpwhich serves as a local MCP server, providing a bridge between AI agents and the WordPress design system implementation.Related: #74626
Note that the current manifests have some existing issues that need to be resolved for this to work well (e.g. #77112)
Why?
Agents are a common part of many developer's workflow, but often struggle to achieve the consistency we hope to see with a design system in recommending guidance and offerings (components, tokens). The goal of this new package is to provide a simple setup experience for someone already using AI agents in development to make those guidance and offerings more readily available, while neither increasing our own maintenance burden nor favoring one documentation vehicle over another (see "How" below).
How?
This leverages existing resources that can be reliably assured as up-to-date:
It's implemented as a npm Node binary, which works well with
mcp.jsonconfiguration. This also avoids the need for external MCP server hosting. This is a common setup experience for other projects offering MCP (e.g. Chakra UI).I had explored an alternative approach leveraging WordPress Abilities API + mcp-adapter which, while an interesting example of "dog-fooding", may not be a great fit:
Testing Instructions
The ideal workflow (i.e. referencing the published npm package) is only possible to test once this is merged and published.
In the meantime, you can test a couple ways:
npm run buildto build filesnpx @modelcontextprotocol/inspector node packages/design-system-mcp/bin/design-system-mcp.mjsget_components,get_component_details,get_design_tokensmcp.json, pointing to the local built file:claude mcp add wordpress-design-system -- node /path/to/gutenberg/packages/design-system-mcp/bin/design-system-mcp.mjsScreenshots or screencast
MCP Inspector example:
Claude Code example:
Use of AI Tools
Claude Code + Claude Opus 4.6, plus a fair bit of back-and-forth and post-processing manual edits. Used for both research of approach and package implementation.