A Model Context Protocol (MCP) server that exposes the 23 Gang of Four design patterns to AI coding agents — generation, canonical examples, AST-based detection, validation, and anti-pattern refactoring for Python codebases.
This is the Python sibling of java-patterns-mcp.
Same 7-tool API, same intent — but every example, detector, validator, and
refactoring is rewritten around Python idioms (__new__, dataclass,
abc.ABC, generators, weakref.WeakSet, functools.singledispatch, …).
Generic LLMs can describe design patterns, but their generated Python code
often misses Pythonic alternatives — a class-based Strategy when a callable
parameter would do, a metaclass Singleton when a module would suffice, a
hand-rolled iterator when a generator function is one line shorter. And no
LLM can deterministically scan a real codebase to say "this class is a
half-broken Singleton because __init__ re-runs on every call".
This MCP server fills that gap with deterministic, AST-backed tooling.
Every entry in patterns.json carries a python_note that the agent reads
through list_patterns. The notes tell the LLM when not to reach for
the classic GoF recipe — because Python has a better idiom for it.
| Category | Pattern | Classic GoF | Pythonic alternative the catalog suggests |
|---|---|---|---|
| Creational | Abstract Factory | abstract Factory + concrete factory classes |
abc.ABC makes the contract explicit; concrete factories typically live as module-level singletons |
| Creational | Builder | dedicated XBuilder with telescoping methods |
keyword arguments + @dataclass; Builder only justified for fluent chaining, staged construction, or cross-field validation |
| Creational | Factory Method | abstract Creator.createX() |
a classmethod factory or a plain module-level function — duck typing means clients don't need the interface |
| Creational | Prototype | abstract clone() on every product |
copy.copy / copy.deepcopy — the pattern is essentially built into the language |
| Creational | Singleton | private ctor + static getter + metaclass | a module-level object (modules are cached in sys.modules); __new__ override if a class is really required |
| Structural | Adapter | wrapper class with a translated interface | often unnecessary — duck typing lets you call the original directly; a thin wrapper function usually suffices |
| Structural | Bridge | abstraction × implementation hierarchies | composition over inheritance — plain composition with a Protocol or ABC defining the implementor |
| Structural | Composite | abstract Component + Leaf + Composite |
duck typing lets Leaf and Composite share an implicit interface; abc.ABC only when you want the contract explicit |
| Structural | Decorator | Decorator base wrapping an Operation |
NOT the same as @decorator syntax (that's definition-time); object-Decorator is justified when you need to stack behaviour at runtime |
| Structural | Facade | Facade class hiding subsystem details |
a module's __init__.py is a natural facade — only the curated names go into __all__ |
| Structural | Flyweight | flyweight factory + intrinsic/extrinsic split | sys.intern() for strings, functools.lru_cache for functions, weakref.WeakValueDictionary for objects |
| Structural | Proxy | proxy class forwarding every method | __getattr__ / __getattribute__ make transparent proxies trivial; weakref.proxy is a built-in lifetime proxy |
| Behavioral | Chain of Responsibility | Handler base + set_next() |
a list of callables and a small dispatch loop is usually enough |
| Behavioral | Command | Command interface + execute() |
a callable + closure already captures "a request"; full Command shines when you need undo() + serialisability |
| Behavioral | Interpreter | one class per grammar rule | rarely needed in pure form — Python's own ast module is a fine reference when you do need it |
| Behavioral | Iterator | Iterator interface with next() |
the iterator protocol (__iter__ / __next__) is native; generator functions collapse it to a single yield |
| Behavioral | Mediator | central Mediator wired to every colleague |
often combined with an EventBus / pub-sub; asyncio.Queue is a lightweight mediator for coroutines |
| Behavioral | Memento | Memento + Caretaker classes |
copy.deepcopy + a stack; or __getstate__ / __setstate__ (the pickle protocol) for richer cases |
| Behavioral | Observer | Subject + Observer interfaces |
weakref.WeakSet to avoid listener leaks; snapshot the subscriber list before iterating to survive concurrent unsubscribes |
| Behavioral | State | one class per state | a callable stored on the context — first-class functions are an alternative to a class hierarchy |
| Behavioral | Strategy | abstract Strategy + concrete classes |
a callable parameter (sorted(xs, key=fn)); class-based Strategy only when the strategy carries state |
| Behavioral | Template Method | abstract base with final algorithm + hooks |
abc.ABC lets you mark only the variable steps as @abstractmethod, freezing the algorithm in the concrete method |
| Behavioral | Visitor | accept(visitor) on every element |
functools.singledispatch — dispatch on the runtime type of the first argument; ast.NodeVisitor is a built-in example |
The detectors and validators are tuned for this. For example, the Strategy
detector intentionally flags an abstract *Strategy class only if there are
two or more concrete subclasses — so the agent never gets nagged about
a one-variant "Strategy" that should just be a function.
| Tool | What it does |
|---|---|
ping |
Health check — server name, version, registered tools |
list_patterns |
All 23 GoF patterns with intent / problem / Python note / refactoring.guru URL |
pattern_examples |
Canonical, runnable Pythonic example source(s) for a pattern |
generate_pattern |
Render a customised implementation (your type_name, your module_name) |
detect_pattern |
AST-based detection: inline source, file list, or recursive directory scan |
validate_pattern |
Pattern-specific code-quality check (ERROR / WARNING / INFO) |
refactor_to_pattern |
Idempotent AST rewrite — turn anti-pattern code into proper pattern |
| Capability | Supported patterns |
|---|---|
list_patterns |
All 23 GoF patterns |
pattern_examples |
All 23 GoF patterns |
generate_pattern |
All 23 GoF patterns |
detect_pattern |
All 23 GoF patterns |
validate_pattern |
18 patterns (Group A + Group B; Group C — Prototype, Flyweight, Interpreter, Iterator, Memento — is deliberately detection-only, mirroring Java COVERAGE.md) |
refactor_to_pattern |
13 idempotent recipes across 11 patterns |
Legend: A = full 4-tool + refactor · B = 4-tool, no refactor · C = detection-only.
| Pattern | Category | Tier | Examples | Generator | Detector | Validator | Refactorings |
|---|---|---|---|---|---|---|---|
| Singleton | Creational | A | ✅ | ✅ | ✅ | ✅ | singleton-add-new-guard, singleton-add-init-once-guard, singleton-add-threading-lock, singleton-add-get-instance |
| Factory Method | Creational | A | ✅ | ✅ | ✅ | ✅ | factory-method-make-creator-abstract |
| Builder | Creational | A | ✅ | ✅ | ✅ | ✅ | (no reducible recipe) |
| Adapter | Structural | A | ✅ | ✅ | ✅ | ✅ | adapter-privatize-adaptee |
| Decorator | Structural | A | ✅ | ✅ | ✅ | ✅ | decorator-privatize-wrappee |
| Composite | Structural | A | ✅ | ✅ | ✅ | ✅ | composite-privatize-children |
| Proxy | Structural | A | ✅ | ✅ | ✅ | ✅ | proxy-privatize-subject |
| Command | Behavioral | A | ✅ | ✅ | ✅ | ✅ | command-mark-implementations-final |
| Observer | Behavioral | A | ✅ | ✅ | ✅ | ✅ | observer-snapshot-iteration |
| State | Behavioral | A | ✅ | ✅ | ✅ | ✅ | state-mark-implementations-final |
| Strategy | Behavioral | A | ✅ | ✅ | ✅ | ✅ | (no reducible recipe) |
| Template Method | Behavioral | A | ✅ | ✅ | ✅ | ✅ | template-method-mark-final |
| Abstract Factory | Creational | B | ✅ | ✅ | ✅ | ✅ | — |
| Bridge | Structural | B | ✅ | ✅ | ✅ | ✅ | — |
| Facade | Structural | B | ✅ | ✅ | ✅ | ✅ | — |
| Chain of Responsibility | Behavioral | B | ✅ | ✅ | ✅ | ✅ | — |
| Mediator | Behavioral | B | ✅ | ✅ | ✅ | ✅ | — |
| Visitor | Behavioral | B | ✅ | ✅ | ✅ | ✅ | — |
| Prototype | Creational | C | ✅ | ✅ | ✅ | — | — |
| Flyweight | Structural | C | ✅ | ✅ | ✅ | — | — |
| Interpreter | Behavioral | C | ✅ | ✅ | ✅ | — | — |
| Iterator | Behavioral | C | ✅ | ✅ | ✅ | — | — |
| Memento | Behavioral | C | ✅ | ✅ | ✅ | — | — |
See COVERAGE.md for the same matrix plus the Java-parity delta.
Every example below shows the tools/call arguments and a (truncated)
result.content[0].text payload. The server always returns JSON-encoded
text, so the agent's call wrapper can json.loads it directly.
Arguments: none.
→ {}
← {
"server": "python-patterns-mcp",
"version": "0.1.0",
"tools": ["ping", "list_patterns", "pattern_examples", "generate_pattern",
"detect_pattern", "validate_pattern", "refactor_to_pattern"]
}Arguments:
category(optional) —"Creational","Structural","Behavioral", or any case variant. Omit to get all 23.
→ { "category": "Creational" }
← {
"count": 5,
"patterns": [
{
"id": "SINGLETON", "display_name": "Singleton", "slug": "singleton",
"category": "Creational",
"intent": "Ensures a class has only one instance …",
"problem": "You need exactly one instance of a class …",
"python_note": "Python idiom: a module-level object is already a singleton …",
"aliases": ["Borg"],
"reference_url": "https://refactoring.guru/design-patterns/singleton"
},
…
]
}Arguments:
pattern(required) — enum name ("SINGLETON"), slug ("singleton"), or display name ("Singleton"). Case-insensitive.include_source(optional, defaulttrue) — set tofalsefor a metadata-only response (lighter token cost when you only need filenames).
→ { "pattern": "observer", "include_source": false }
← {
"pattern": {
"id": "OBSERVER", "display_name": "Observer", "slug": "observer",
"category": "Behavioral",
"reference_url": "https://refactoring.guru/design-patterns/observer",
"python_note": "Iterate over a snapshot of subscribers when emitting …"
},
"example_count": 1,
"files": [{ "file_name": "event_bus.py",
"relative_path": "examples/observer/event_bus.py",
"source": null }]
}Arguments:
pattern(required) — one ofsingleton,builder,strategy,observer.type_name(required) — the main class name (e.g."Logger","Pizza"). Must be a valid Python identifier.module_name(optional) — overrides the snake_case derived fromtype_name. Used in docstrings/comments only.
→ { "pattern": "singleton", "type_name": "Config" }
← {
"pattern": "SINGLETON", "type_name": "Config", "module_name": "",
"file_count": 1,
"files": [{
"file_name": "config.py",
"source": "\"\"\"Thread-safe Singleton — generated by python-patterns-mcp.\n…\"\"\"\n\nimport threading\n\nclass Config:\n _instance: \"Config | None\" = None\n _lock: threading.Lock = threading.Lock()\n\n def __new__(cls, *args, **kwargs) -> \"Config\":\n if cls._instance is None:\n with cls._lock:\n …\n"
}]
}Unsupported patterns return a clear error pointing at pattern_examples:
→ { "pattern": "visitor", "type_name": "AstWalker" }
← ToolError: "Generation not yet implemented for VISITOR. Try pattern_examples instead. Supported: BUILDER, OBSERVER, SINGLETON, STRATEGY"Arguments (pass exactly one of):
source— inline Python source as a single string.paths— list of absolute / relative.pyfile paths on disk.directory— directory to scan recursively (cap: 1000.pyfiles).
→ {
"source": "import threading\nclass Logger:\n _instance = None\n _lock = threading.Lock()\n def __new__(cls):\n if cls._instance is None:\n with cls._lock:\n if cls._instance is None:\n cls._instance = super().__new__(cls)\n return cls._instance\n @classmethod\n def get_instance(cls): return cls()\n"
}
← {
"file_count": 1, "hit_count": 1, "errors": [],
"supported_patterns": ["SINGLETON", "BUILDER", "STRATEGY", "OBSERVER"],
"hits": [{
"pattern": "SINGLETON", "display_name": "Singleton",
"location": "Logger", "file": "<inline>", "line": 2,
"confidence": 0.95,
"signals": [
"class attribute '_instance' initialised to None",
"__new__ returns cached instance",
"get_instance() factory method"
]
}]
}Parse failures (per file) are reported in errors and never abort the batch.
Arguments:
source(required) — Python source to inspect.pattern(optional) — narrow the check to one pattern; omit to run every supported validator.
→ {
"pattern": "singleton",
"source": "class Logger:\n _instance = None\n def __new__(cls):\n if cls._instance is None:\n cls._instance = super().__new__(cls)\n return cls._instance\n def __init__(self):\n self.log = []\n"
}
← {
"scope": "SINGLETON", "issue_count": 3,
"errors": [],
"warnings": [
{ "severity": "WARNING", "location": "Logger", "line": 1,
"message": "Singleton class defines both __new__ and __init__. __init__ runs on every instantiation …",
"suggestion": "Either guard __init__ with 'if self._initialized: return', or move initialisation into a private _initialize() method called once from __new__." },
{ "severity": "WARNING", "location": "Logger", "line": 3,
"message": "__new__ does not appear to use a threading.Lock. Two threads calling the class concurrently can each create a separate instance …",
"suggestion": "Add a class-level 'threading.Lock' and a double-checked lock around the 'if cls._instance is None' branch." }
],
"infos": [
{ "severity": "INFO", "location": "Logger", "line": 1,
"message": "No get_instance() classmethod found. Calling the class directly works …",
"suggestion": "Add 'def get_instance(cls) -> Self: return cls()' as a @classmethod for readability." }
]
}Arguments:
source(required) — Python source to rewrite.refactoring(required) — one ofsingleton-add-new-guard,singleton-add-init-once-guard.
→ {
"refactoring": "singleton-add-init-once-guard",
"source": "class Logger:\n _instance = None\n def __new__(cls):\n if cls._instance is None:\n cls._instance = super().__new__(cls)\n return cls._instance\n def __init__(self):\n self.log = []\n"
}
← {
"refactoring": "singleton-add-init-once-guard",
"changed": true,
"log": ["Added once-only __init__ guard to class 'Logger' at line 7"],
"source": "class Logger:\n _instance = None\n\n def __new__(cls):\n if cls._instance is None:\n cls._instance = super().__new__(cls)\n return cls._instance\n\n def __init__(self):\n if getattr(self, '_initialized', False):\n return\n self._initialized = True\n self.log = []\n",
"supported_refactorings": ["singleton-add-init-once-guard", "singleton-add-new-guard"]
}Re-running the same refactoring on already-correct code is a no-op
(changed: false, empty log).
A typical conversation: "clean up this Logger so it's actually a singleton".
1. detect_pattern { source: <user's code> }
→ SINGLETON @ Logger, confidence 0.6 — only 1 signal fired
2. validate_pattern { source: …, pattern: "singleton" }
→ 2 WARNINGs (__init__ re-runs, no Lock) + 1 INFO (no get_instance)
3. refactor_to_pattern { source: …, refactoring: "singleton-add-init-once-guard" }
→ rewritten source with `_initialized` guard at the top of __init__
4. validate_pattern { source: <rewritten>, pattern: "singleton" }
→ only the Lock warning + get_instance INFO remain
→ agent decides to add a threading.Lock by hand, then declares done
- Python 3.11+ (uses
Self,slots=Truedataclasses,match-friendly enums) mcpSDK ≥ 1.2.0 (providesFastMCP)jinja2≥ 3.1.0 (forgenerate_pattern)
Pick whichever Python package manager you already have. All three give you the same working server.
git clone https://github.com/<you>/python-patterns-mcp.git
cd python-patterns-mcp
uv venv
uv pip install -e ".[dev]"The interpreter for OpenCode wiring is then .venv/bin/python.
If you just want to use the server (not develop on it):
pipx install git+https://github.com/<you>/python-patterns-mcp.gitpipx exposes the python-patterns-mcp console script on your $PATH,
which you can plug straight into OpenCode without any path gymnastics:
git clone https://github.com/<you>/python-patterns-mcp.git
cd python-patterns-mcp
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"Behind a corporate
pip.conf? Some SAP/internal mirrors fail name resolution off-VPN andpiphangs on retries. Bypass with:PIP_CONFIG_FILE=/dev/null pip install -e ".[dev]"
python -m python_patterns_mcpThe server speaks the standard MCP stdio transport — stdin for JSON-RPC requests, stdout for JSON-RPC responses, stderr for all log lines (so stdout stays clean). It blocks until stdin closes.
A convenience console script is also installed: python-patterns-mcp.
python tests/smoke_stdio.pyThis script boots the server as a subprocess, exchanges 7 JSON-RPC frames,
and prints the parsed responses for initialize, tools/list, ping,
list_patterns, detect_pattern, generate_pattern, and pattern_examples.
Expected tail:
Received 7 response(s) on stdout
[initialize] serverInfo={'name': 'python-patterns-mcp', 'version': '0.1.0'}
[tools/list] 7 tools: ping, list_patterns, pattern_examples, generate_pattern,
detect_pattern, validate_pattern, refactor_to_pattern
[ping] {'server': 'python-patterns-mcp', 'version': '0.1.0', ...}
[list_patterns] 5 entries
[detect_pattern] 1 hit(s); errors=[]
• SINGLETON @ Logger (conf=0.95, signals=3)
[generate_pattern] produced 1 file: config.py
[pattern_examples] pattern=ITERATOR, files=1: name_roster.py
pytest -v
# 117 passed in 0.6sThe test suite covers every tool end-to-end. A particularly satisfying test
is test_refactored_singleton_runs: it takes a plain class, applies two
refactorings in sequence, executes the result, and asserts the rewritten
class genuinely behaves like a singleton.
Add to ~/.config/opencode/opencode.json:
{
"mcp": {
"python-patterns": {
"type": "local",
"command": [
"/Users/<you>/git/com/python-patterns-mcp/.venv/bin/python",
"-m",
"python_patterns_mcp"
]
}
}
}OpenCode then sees all 7 tools under the python-patterns namespace.
python-patterns-mcp/
├── pyproject.toml
├── README.md
├── LICENSE
├── resources/
│ ├── catalog/patterns.json ← refactoring.guru-style metadata
│ ├── examples/<slug>/*.py ← canonical Pythonic examples
│ └── templates/<slug>/*.py.jinja ← code-generation templates
├── src/python_patterns_mcp/
│ ├── __init__.py
│ ├── __main__.py ← `python -m python_patterns_mcp`
│ ├── server.py ← FastMCP bootstrap
│ ├── catalog/ ← Pattern enum + registry + examples
│ ├── tools/ ← 7 MCP tool handlers
│ ├── generate/ ← Jinja2 generator
│ ├── detect/ ← AST-based detectors
│ ├── validate/ ← pattern-quality validators
│ └── refactor/ ← AST rewriters
└── tests/
├── test_catalog.py
├── test_examples.py
├── test_generator.py
├── test_detect.py
├── test_validate.py
├── test_refactor.py
├── test_server.py
└── smoke_stdio.py ← end-to-end stdio smoke test
The architecture lets you grow any of the five capability tables just by adding one file per pattern — no changes to the existing engines. The initial full-parity milestone is done — every GoF pattern has an example, a generator, and a detector, and Group A + B additionally have validators. What's still open is either out of scope or quality improvement rather than "coverage".
validate_patternfor Group C (Prototype, Flyweight, Interpreter, Iterator, Memento) — matches Java'sCOVERAGE.mddecision. The pitfalls in these five patterns are too diffuse to reduce to a small set of actionable rules; adding validators here tends to produce noise.refactor_to_patternfor Group B (Abstract Factory, Bridge, Facade, Visitor, Chain of Responsibility, Mediator) — these patterns' typical fixes require design-level judgement, not a one-line AST edit.
- Detector confidence tuning — every pattern already emits a
0.0..1.0 score and a
signals[]breakdown. Fine-tuning the base + per-signal weights against a wider corpus of real Python code would reduce false positives further. - More validator rules — e.g. Observer listener leak (missing
weakref), Iterator single-shot detection, Decorator delegation completeness, Factory Method concrete-override coverage. - Custom refactorings — e.g.
builder-make-fields-final(convert a Builder's product into a frozen@dataclass(slots=True)).
- GitHub Actions CI (mirror the Java repo's
mvn verifysetup) - PyPI publish workflow
mcp dev/ MCP Inspector wiring for interactive exploration- More tests around the
directorymode ofdetect_pattern(large recursive scans, parse-error reporting)
PRs welcome. The simplest way to contribute is to open an issue with a real-world Python snippet where a detector misfires — the fix is usually a single-line adjustment to the pattern's signal weights.
MIT © 2026 contributors.
Pattern examples are adapted from refactoring.guru and the original Design Patterns: Elements of Reusable Object-Oriented Software (Gamma, Helm, Johnson, Vlissides). All adapted code is original re-implementation; no third-party source is reproduced verbatim.
{ "mcp": { "python-patterns": { "type": "local", "command": ["python-patterns-mcp"] } } }