DEV Community

AI Dev Hub
AI Dev Hub

Posted on

Building a rules-file MCP server in 2026

Building a rules-file MCP server in 2026

You can build an MCP server that writes CLAUDE.md, .cursorrules, and copilot-instructions.md from one prompt in about 30 lines of Python with FastMCP. Register the tool, point your assistant's config at it, and ask. The code is the easy part. Keeping three formats in sync is the actual work. Here's the server and the part that broke first.

The rules-file generator I link to below is one I built. I'd opened four separate template repos last spring trying to bootstrap a CLAUDE.md, and every one assumed a single assistant. Cursor users had .cursorrules. The Claude crowd had their own file. Nobody emitted every format from one input. So I wrote a small web page that does. It's free, runs entirely in your browser, no signup, nothing gets uploaded. If you've got a better one, tell me.

The goal

A local MCP server that any MCP-aware assistant can call to write its own rules file. You say 'set up the rules for this repo,' the assistant calls the tool with what it already knows about your stack, and three files land on disk:

  • CLAUDE.md at the repo root
  • .cursorrules beside it
  • .github/copilot-instructions.md for the Copilot users

No copy-paste between editor tabs. The outcome you'd screenshot: your assistant printing wrote 412 bytes to CLAUDE.md right in the chat, and the file appearing in your sidebar a half second later.

A rules file is just instructions your assistant loads before it reads your code: your conventions, plus the things it keeps getting wrong. Most teams write one by hand, then never touch it again, and it rots. Generating it from a tool means you can regenerate when the stack changes instead of editing prose at midnight. The sync problem is why I cared. I'd update CLAUDE.md and forget the .cursorrules copy, and then two teammates on different assistants would get different rules. That drift is quiet and it's expensive.

Why a server and not a plain script? Because the assistant can call it mid-conversation, with context it already has. It knows you're on Postgres and Vite before you say a word. That's the whole reason to bother.

Setup and auth

Here's the part nobody warns you about: there's no auth. I spent a chunk of last Thursday hunting for where an API key goes, and there isn't one. A local MCP server talks to your assistant over stdio, on your machine, as your user. The "credential" is filesystem permission. I was wrong about needing a token, and it took me 47 minutes of reading the spec before I believed it.

Install the SDK with pip install "mcp[cli]". Then register the server. For Claude Code, that's an .mcp.json file in the project root with a command and args pointing at your Python file. Cursor and Claude Desktop use their own JSON config, same shape. One gotcha that cost me real time: a stdio server must never write to stdout, because that stream carries the JSON-RPC frames. Any stray print() corrupts the protocol. Log to stderr or to a file.

You can pass a default through the environment too. I set RULES_DEFAULT_STACK in the .mcp.json env block so the tool has a fallback when the model forgets to send one. Small thing, but it turned a class of empty-stack files into a sane default. Restart the assistant after editing the config, by the way: most of them read MCP settings once at launch and won't pick up changes live, which confused me for a good ten minutes the first time.

If you just want the files without running a server, the rules-file generator at aidevhub.io/rules-file-generator produces the same multi-format output in the browser. I reach for it when I'm scaffolding a repo I'll touch once and forget.

The core code

The whole server fits in one file. Here it is, with the guard I added only after it bit me (more on that in the last section):

# server.py
from mcp.server.fastmcp import FastMCP
from pathlib import Path

mcp = FastMCP("rules-file-generator")

TEMPLATES = {
    "claude": "CLAUDE.md",
    "cursor": ".cursorrules",
    "copilot": ".github/copilot-instructions.md",
}

@mcp.tool()
def generate_rules(target: str, project_name: str, stack: str) -> str:
    """Write an AI rules file. target: claude | cursor | copilot."""
    filename = TEMPLATES.get(target)
    if filename is None:  # guard added after the bug in the last section
        raise ValueError(
            f"unknown target {target!r}; pick claude, cursor, or copilot"
        )

    body = (
        f"# {project_name}\n\n"
        f"Stack: {stack}\n\n"
        "## Conventions\n"
        "- Keep functions under 40 lines.\n"
        "- Put tests next to the code they cover.\n"
        "- No new dependency without a note in the PR.\n"
    )
    out = Path(filename)
    out.parent.mkdir(parents=True, exist_ok=True)
    out.write_text(body, encoding="utf-8")
    return f"wrote {len(body)} bytes to {out}"

if __name__ == "__main__":
    mcp.run()
Enter fullscreen mode Exit fullscreen mode

The @mcp.tool() decorator does the heavy lifting. FastMCP reads your type hints (target: str, and the rest) and generates the JSON schema the assistant uses to call the tool correctly. You write a normal Python function and the protocol wiring is generated for you. Return a string and it shows up in the chat as the tool result.

To call it, you don't do anything special. You ask the assistant in plain language ('generate a Claude rules file for this project, we're on FastAPI and Postgres') and it maps that to the generate_rules arguments on its own. The first time you watch a model fill in stack="FastAPI + Postgres" without you naming the parameter, it feels like cheating. That mapping is exactly what the type hints buy you.

One design choice worth calling out: the tool writes the file itself instead of returning the text for the assistant to write. I went back and forth on this. Returning text keeps the server side-effect free, which is cleaner, but then the assistant has to turn around and call its own file-write tool, and you've added a round trip and a chance for it to mangle the content. Writing directly from the server means the bytes that get validated are the bytes that hit disk. For a generator like this, I'd rather own the write.

How FastMCP compares to the alternatives

I tried the TypeScript SDK first and bounced off it. It's fine. I just had a Python repo open and didn't want a Node toolchain to write three files. Here's how the realistic options stack up on the axes I cared about:

Approach Lines to first working tool Schema validation Best when
FastMCP (Python) ~12 inferred from type hints you want a tool running before lunch
TypeScript SDK ~40 explicit, via Zod your stack is already Node
Raw JSON-RPC over stdio ~120 hand-written you need zero dependencies

The Python row wins for this job because the template logic is a dozen lines of string formatting and the SDK gets out of the way. If your assistant tooling already lives in Node, the gap closes and the TypeScript SDK is the saner pick. Raw JSON-RPC is there if you enjoy pain or have a runtime the SDKs don't support yet.

One axis I left off the table on purpose: performance. None of these matter at this scale. You're writing a few hundred bytes once. If you're benchmarking a rules-file generator you've taken a wrong turn somewhere. Pick the SDK that matches the language your tooling already speaks and move on.

What broke the first time I ran it

A KeyError, and it cost me an afternoon. The first version looked up TEMPLATES[target] with a plain bracket, no guard. The model called the tool with target="claude-code" instead of "claude", Python raised a bare KeyError, and MCP wrapped it into a generic "tool failed" with nothing useful in the chat. I sat there convinced the stdio transport was broken. It was a typo in one argument.

The fix is the .get() plus an explicit ValueError that names the valid options. That error string goes straight back to the model, which reads it and retries with "claude". That feedback loop is the real point of returning good errors from a tool: the assistant self-corrects if you let it. Honestly this annoyed me for most of a day before it clicked.

The second thing: that stray print() I mentioned. I'd left one in to debug the KeyError, and it quietly corrupted the JSON-RPC frames. The server "connected" and then every call hung with no error at all. Pull debug output to stderr and it comes back to life. Both bugs were mine, and both took longer to find than the whole server took to write.

FAQ

Q: Do I need a separate server for Cursor and Claude Code?
A: No. It's the same server and the same stdio protocol. You register it in each tool's config file, but the server code doesn't change.

Q: Can the tool read my existing code to infer conventions?
A: Yes. Add a second tool that globs the repo and feeds a short summary into the template. Keep it read-only so a bad prompt can't rewrite files you didn't mean to touch.

Q: Why three files instead of one shared format?
A: Because the assistants haven't agreed on a format. CLAUDE.md is Markdown prose. The .cursorrules format is its own shape, and Copilot wants a specific path under .github. Until that converges, you generate each one.

Q: Is FastMCP ready for real use in 2026?
A: For local developer tooling, yes. I wouldn't expose one to the public internet without real auth in front of it, since the local model assumes a single trusted user.

Written with AI assistance and human review. Try the tool at aidevhub.io/rules-file-generator.

Top comments (0)