Skip to content

Add configurable MCP support for ReSpec report workflows#5168

Closed
danielcamposramos wants to merge 1 commit intospeced:mainfrom
danielcamposramos:codex/respec-mcp-authoring-guidance
Closed

Add configurable MCP support for ReSpec report workflows#5168
danielcamposramos wants to merge 1 commit intospeced:mainfrom
danielcamposramos:codex/respec-mcp-authoring-guidance

Conversation

@danielcamposramos
Copy link
Copy Markdown

Summary

This PR adds a configurable stdio MCP companion for ReSpec so AI tools and editors can scaffold, preflight, validate, and build reports from repo-local profiles.

What Changed

  • add respec-mcp stdio entrypoints and MCP core/server modules
  • add repo-local profile/config discovery via respec-mcp.config.json and respec-mcp/profiles/*.json
  • add Docker runtime for Node 24 + Chromium environments
  • add focused MCP tests
  • add LLM authoring guidance for W3C and Community Group report workflows, including standards-transition patterns and examples
  • expose the authoring guidance path in MCP responses so clients can discover and apply it

Why

The goal is to let Community Groups and similar spec repositories keep local policy and templates while reusing one upstream MCP runtime. The added authoring guidance is meant to make the MCP useful for real standards-oriented writing and review, not only for rendering HTML.

Validation

  • docker build -t respec-mcp:local .
  • docker run --rm --entrypoint node respec-mcp:local /app/node_modules/jasmine/bin/jasmine.js --random=false /app/tests/mcp.cjs

Introduce a stdio MCP interface for ReSpec so editors and AI tools can scaffold, preflight, validate, and build reports from repo-local profiles.

Keep group-specific policy out of the core implementation by loading statuses, templates, required sections, required links, and phrase checks from repository configuration.

Add a Docker runtime for Node 24 and Chromium-based environments so the same workflow can run consistently in local and containerized setups.
@sidvishnoi
Copy link
Copy Markdown
Member

Can you share a video demo to show how it works?

@danielcamposramos
Copy link
Copy Markdown
Author

Can you share a video demo to show how it works?

Hi @sidvishnoi,

Thanks for looking at this. We built this MCP as a side project within the PM-KR Community Group to support our own report authoring workflow. The group's focus is on the main specification work, so we don't have the bandwidth to record a video demo right now — but the MCP is functional and has already been used in practice.

How to use it:
The server runs over stdio, so any MCP-aware client can connect.

VSCode agents (Claude Code, Cline, GitHub Copilot, etc.) — add to your MCP settings:

{
"mcpServers": {
"respec-mcp": {
"command": "node",
"args": ["tools/respec-mcp.js", "--repo-root", "/path/to/your/spec-repo"],
"transport": "stdio"
}
}
}

Docker (includes Node 24 + headless Chromium for respec_build):

docker build -t respec-mcp:local .
docker run --rm -i respec-mcp:local

CLI (directly, for scripting or CI):

node tools/respec-mcp.js --repo-root /path/to/your/spec-repo

Once connected, the agent gets five tools:
respec_list_profiles, respec_scaffold, respec_preflight, respec_validate, and respec_build.
Each consuming repo provides its own respec-mcp.config.json and respec-mcp/profiles/*.json to define allowed statuses, required sections, forbidden phrases, templates, and build paths — so group-specific policy stays in the repo, not in the MCP server.

Live artifact
The PM-KR Standards Track Transition Report was drafted using this setup with LLM assistance (Codex). The PM-KR repo at w3c-cg/pm-kr has a working respec-mcp.config.json and profile that can serve as a reference for other groups.

Happy to answer any questions about the implementation or integration patterns.

@danielcamposramos danielcamposramos marked this pull request as ready for review April 10, 2026 18:52
@marcoscaceres
Copy link
Copy Markdown
Contributor

Will take a look soon…

@marcoscaceres
Copy link
Copy Markdown
Contributor

marcoscaceres commented Apr 17, 2026

Thanks for putting this together, Daniel. The profile/policy pattern is a genuinely interesting idea: letting each repo declare its own allowed statuses, required sections, forbidden phrases, and templates is a clean way to handle CG governance without forking ReSpec. The references in the authoring guide are solid too. I checked them all and they hold up.

That said, I have concerns in two areas: security and packaging.

Security

Path traversal / arbitrary file write: There are no containment checks on any resolved paths. Every tool that accepts repo_root, source, or output passes them straight through path.resolve() without verifying the result stays within the repo root. For example, in resolveScaffoldPath():

if (output) {
  return path.resolve(state.repoRoot, output);
}

An MCP client (i.e., an LLM) sending output: "../../.bashrc" or output: "/etc/crontab" would write to those locations. The mkdir({ recursive: true }) call before each writeFile makes this worse, since it creates any intermediate directories needed. The same applies to readSourceText(), which will read any file on the filesystem if given an absolute path.

Since MCP tool inputs come from LLMs (which can be manipulated via prompt injection from document content), this is a meaningful attack surface. Every path.resolve() result that feeds into readFile, writeFile, or URL construction needs to be validated to stay within the repo root.

Unrestricted URL navigation: toSourceUrl() allows source to be any URL scheme. If an LLM provides source: "https://evil.example.com/exploit.html", Puppeteer will navigate Chromium to that URL. Combined with --disable-sandbox, this is a code execution risk. The server should restrict source to file:// URLs rooted within repoRoot, or relative paths within it.

repo_root is LLM-overridable: Every tool's Zod schema accepts repo_root: z.string().optional(). An LLM can override the CLI-provided --repo-root to any directory (e.g., /). The CLI-provided root should be the enforced boundary, not something the client can override per-call.

Prototype pollution via overrides: The schema uses z.record(z.any()) and the values are spread directly into objects:

const merged = {
  ...repoDefaults,
  ...profileDefaults,
  ...overrides,  // LLM-controlled
};

Keys like __proto__, constructor, prototype should be filtered before spreading. V8's spread operator currently doesn't copy __proto__ from plain objects, but relying on engine-specific behavior for security is fragile.

Design / Packaging

Separate package, not bundled in core: @modelcontextprotocol/sdk pulls in ~30 transitive dependencies including Express 5, Hono, jose (JWT), pkce-challenge (OAuth), and more. These are added as production dependencies, meaning every consumer of ReSpec as an npm package inherits the entire tree even if they never use the MCP server. This would work better as a separate package that depends on respec for respecDocWriter.js but ships independently.

validate vs preflight are identical: Both call renderAndAssess(input, options, { writeOutput: false }). From an MCP client's perspective, there's no behavioral difference. Either differentiate them (e.g., preflight does fast source-only checks without rendering, validate does the full Puppeteer render) or merge them into one.

Compliance checks are shallow: Required sections are detected via htmlLower.includes(section.toLowerCase()), which means the word "Introduction" appearing anywhere in the document body would satisfy a required-section check for <h2>Introduction</h2>. Same issue for required links and forbidden phrases. For forbidden phrases in particular, a false positive could block a build unnecessarily.

Happy to discuss any of this further.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new respec-mcp stdio MCP server that wraps ReSpec rendering and adds repo-local profile/config discovery so AI tools can scaffold, validate, preflight, and build spec reports using repository-defined policies and templates.

Changes:

  • Add MCP server + core workflow modules (listProfiles, scaffoldSource, validate/preflight/build) and a new respec-mcp CLI entrypoint.
  • Add MCP-focused Jasmine tests and wire them into pnpm test.
  • Add documentation (MCP usage + LLM authoring guidance) and a Docker image intended for a Node 24 + Chromium runtime.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tools/respecMcpServer.js Registers MCP tools and formats MCP responses.
tools/respecMcpCore.js Implements repo config/profile discovery, templated scaffolding, rendering, and compliance checks.
tools/respec-mcp.js New stdio CLI entrypoint (respec-mcp) that boots the MCP server.
tests/mcp.cjs Adds focused tests for profile discovery, scaffolding, building, and preflight compliance.
package.json Exposes new bin/script entries and adds MCP dependencies.
pnpm-lock.yaml Locks newly added dependencies (@modelcontextprotocol/sdk, zod, transitive deps).
docs/MCP.md Documents MCP configuration contract and usage patterns.
docs/MCP_LLM_AUTHORING_GUIDE.md Adds authoring guidance intended to be discoverable by MCP clients.
README.md Publicly advertises the new MCP server and points to docs.
Dockerfile Adds a Docker runtime intended for MCP usage in a Chromium-capable environment.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tools/respecMcpCore.js
Comment on lines +323 to +327
if (/^\w+:\/\//.test(sourceRef)) {
const response = await fetch(sourceRef);
if (!response.ok) {
throw new Error(`Failed to fetch source ${sourceRef}: ${response.status}`);
}
Comment thread tools/respecMcpCore.js
}

function applyTemplate(template, values) {
return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_match, key) => {
Comment thread Dockerfile
RUN corepack enable \
&& pnpm install --frozen-lockfile

ENTRYPOINT ["node", "tools/respec-mcp.js", "--repo-root", "/workspace"]
Comment thread package.json
"zod": "^3.25.76"
},
"files": [
"builds/",
@marcoscaceres
Copy link
Copy Markdown
Contributor

After thinking about this more, I think the right next step would be to incubate this in a separate repository rather than merging it into the ReSpec core package. There's precedent for this in the speced org: both ReSpec and Bikeshed split out specialized concerns into their own repos (e.g., respec-web-services, bikeshed-data, bikeshed-boilerplate).

A separate repo like speced/respec-mcp would let you iterate on the profile/policy design and address the security concerns without adding ~30 transitive dependencies to every ReSpec install. It can import respecDocWriter.js from respec as a dependency and ship on its own release cycle.

Happy to help set that up if you'd like.

@marcoscaceres
Copy link
Copy Markdown
Contributor

Closing this out... but happy to keep discussing here.

@danielcamposramos danielcamposramos deleted the codex/respec-mcp-authoring-guidance branch April 20, 2026 17:23
@danielcamposramos
Copy link
Copy Markdown
Author

Hi @marcoscaceres — thanks for the careful review. You were right that this belonged in its own repo. I took the feedback seriously and rebuilt it as a standalone package:

https://github.com/danielcamposramos/respec-mcp

Every concern you and Copilot raised is addressed. Summary for the record:

Security (your blockers)

  • Path containment. Every path input (source, output, repo_metadata_source, templates, profile directory) resolves through a resolveWithinRoot helper that rejects anything escaping --repo-root (including absolute paths outside the root, .. traversal, and null bytes). Unit tests cover each case.
  • URL restriction for Puppeteer. source accepts only relative paths inside the repo root or file:// URLs pointing inside it. http(s), data:, javascript:, and other schemes are rejected before Puppeteer is ever handed a URL.
  • repo_root is no longer part of the tool input schema. The CLI flag is the sole boundary — the MCP client cannot override it per call.
  • Prototype-pollution hardening. overrides, template_defaults, and respec_defaults merge into an Object.create(null) target with a key filter that drops __proto__, constructor, and prototype. Template placeholder resolution also refuses to traverse into __proto__. Tests include a JSON.parse('{"__proto__": {...}}') adversarial input.

Design

  • preflight vs validate are now distinct. preflight is source-only, no Puppeteer — fast feedback before editing. validate is the full ReSpec render via Puppeteer with diagnostics but no write. build is render + write. The README table makes the trade-off explicit.
  • Compliance checks use DOM parsing (linkedom), not String.includes(). Required sections are matched against <h1>..<h6> text (ReSpec's .secno / .self-link nodes are stripped first so auto-numbering like "1. Introduction" still matches Introduction). Required links are matched against <a href> values. Forbidden phrases use word-boundary matching on visible text with scripts/styles removed — so "commit" doesn't match "uncommittable" and a W3C Recommendation string inside a <script> no longer trips a false positive.

Packaging

  • Standalone npm package. respec is a runtime dependency (via respec/tools/respecDocWriter.js), not a parent. ReSpec core ships none of the MCP SDK's transitive tree.
  • Dockerfile runs as a non-root user (uid 10001) and bakes --disable-sandbox into the ENTRYPOINT so Chromium starts under an unprivileged UID.
  • docs/ is in the published files list, so MCP responses referencing the authoring guide resolve for npm consumers. The guide is also exposed as an MCP resource (respec-mcp://authoring-guide) so clients can fetch it over the wire.

Copilot's items

  • file:// sources are read via fileURLToPath + readFile, not fetch().
  • Template placeholders with dotted keys ({{editors.primary.name}}) now resolve against nested objects.
  • Dockerfile: see above — non-root + --disable-sandbox.
  • docs/ shipped in npm package: see above.

Also

  • Added W3C explainer support per https://www.w3.org/TR/explainer-explainer/: authoring-guide section, and a second worked profile (examples/example-explainer/) that enforces the TAG explainer skeleton via required_sections and blocks spec-like status claims via forbidden_phrases.
  • 34 unit specs (security, template, compliance, core) + 3 integration specs (full Puppeteer render) — all passing. CI matrix runs Node 20 / 22 / 24.

If you're up for taking this under the speced/ umbrella (transfer or fork — whichever is simpler for you), I'd be glad to hand it over. Happy to iterate on naming, API, or anything else. No pressure either way; I'll maintain it under my own namespace in the interim so PM-KR and similar groups can use it.

@marcoscaceres
Copy link
Copy Markdown
Contributor

Do you want some time for the community to experiment? I think it would be great to see what you all come up with. Having built several MCPs, I know how rapidly they can evolve.

How about you check-in in 6 weeks or so once you’ve had a chance for the design to stabilize and have gained some usage experience across the CG? No objections to setting up a repo for you. But would like to be sure you’ve gained significant experience with it first. Then we can bring it over piecemeal and go hard on privacy, security, performance, etc. audits to make it generally available if it proves useful.

I’ll try to find time to play with it and provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants