Skip to main content
Glama
README.md
# python-patterns-mcp

> 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.

[![Python 3.11+](https://img.shields.io/badge/Python-3.11%2B-blue)](https://www.python.org/)
[![MCP](https://img.shields.io/badge/MCP-1.x-purple)](https://modelcontextprotocol.io)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)

This is the Python sibling of [java-patterns-mcp](https://github.com/air237/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`, …).

---

## Why a Python version

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**.

## Pythonic decisions baked into the catalog

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.

## Tools

| 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 |

### Coverage in this build

| 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 |

#### Per-pattern feature matrix

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`](./COVERAGE.md) for the same matrix plus the Java-parity delta.

## Tools — full reference with examples

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.

### 1. `ping` — health check

**Arguments:** none.

```json
→ {}
← {
  "server": "python-patterns-mcp",
  "version": "0.1.0",
  "tools": ["ping", "list_patterns", "pattern_examples", "generate_pattern",
            "detect_pattern", "validate_pattern", "refactor_to_pattern"]
}
```

### 2. `list_patterns` — catalog of all 23 GoF patterns

**Arguments:**
- `category` *(optional)* — `"Creational"`, `"Structural"`, `"Behavioral"`,
  or any case variant. Omit to get all 23.

```json
→ { "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"
    },
    …
  ]
}
```

### 3. `pattern_examples` — canonical Pythonic examples

**Arguments:**
- `pattern` *(required)* — enum name (`"SINGLETON"`), slug (`"singleton"`),
  or display name (`"Singleton"`). Case-insensitive.
- `include_source` *(optional, default `true`)* — set to `false` for a
  metadata-only response (lighter token cost when you only need filenames).

```json
→ { "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 }]
}
```

### 4. `generate_pattern` — render a customised implementation

**Arguments:**
- `pattern` *(required)* — one of `singleton`, `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 from
  `type_name`. Used in docstrings/comments only.

```json
→ { "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`:

```json
→ { "pattern": "visitor", "type_name": "AstWalker" }
← ToolError: "Generation not yet implemented for VISITOR. Try pattern_examples instead. Supported: BUILDER, OBSERVER, SINGLETON, STRATEGY"
```

### 5. `detect_pattern` — AST-based scan

**Arguments (pass exactly one of):**
- `source` — inline Python source as a single string.
- `paths` — list of absolute / relative `.py` file paths on disk.
- `directory` — directory to scan recursively (cap: 1000 `.py` files).

```json
→ {
  "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.

### 6. `validate_pattern` — flag implementation pitfalls

**Arguments:**
- `source` *(required)* — Python source to inspect.
- `pattern` *(optional)* — narrow the check to one pattern; omit to run
  every supported validator.

```json
→ {
  "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." }
  ]
}
```

### 7. `refactor_to_pattern` — idempotent AST rewrite

**Arguments:**
- `source` *(required)* — Python source to rewrite.
- `refactoring` *(required)* — one of
  `singleton-add-new-guard`, `singleton-add-init-once-guard`.

```json
→ {
  "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`).

## End-to-end agent workflow

A typical conversation: *"clean up this Logger so it's actually a singleton"*.

```text
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
```

## Requirements

- **Python 3.11+** (uses `Self`, `slots=True` dataclasses, `match`-friendly enums)
- **`mcp` SDK** ≥ 1.2.0 (provides `FastMCP`)
- **`jinja2`** ≥ 3.1.0 (for `generate_pattern`)

## Install

Pick whichever Python package manager you already have. All three give you
the same working server.

### Option A — `uv` (fastest, recommended)

```bash
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`.

### Option B — `pipx` (one-shot, isolated, no venv juggling)

If you just want to *use* the server (not develop on it):

```bash
pipx install git+https://github.com/<you>/python-patterns-mcp.git
```

`pipx` exposes the `python-patterns-mcp` console script on your `$PATH`,
which you can plug straight into OpenCode without any path gymnastics:

```jsonc
{ "mcp": { "python-patterns": { "type": "local",
           "command": ["python-patterns-mcp"] } } }
```

### Option C — plain `pip` + `venv`

```bash
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 and `pip` hangs on retries. Bypass with:
> ```bash
> PIP_CONFIG_FILE=/dev/null pip install -e ".[dev]"
> ```

## Run

```bash
python -m python_patterns_mcp
```

The 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`.

## Smoke test

```bash
python tests/smoke_stdio.py
```

This 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
```

## Unit tests

```bash
pytest -v
# 117 passed in 0.6s
```

The 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.

## Wire into OpenCode

Add to `~/.config/opencode/opencode.json`:

```jsonc
{
  "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.

## Project layout

```
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
```

## Roadmap

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".

### Out-of-scope

* **`validate_pattern` for Group C** (Prototype, Flyweight, Interpreter,
  Iterator, Memento) — matches Java's `COVERAGE.md` decision. 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_pattern` for 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.

### Nice-to-haves

* **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)`).

### Project-level work

* GitHub Actions CI (mirror the Java repo's `mvn verify` setup)
* PyPI publish workflow
* `mcp dev` / MCP Inspector wiring for interactive exploration
* More tests around the `directory` mode of `detect_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.

## License

[MIT](./LICENSE) © 2026 contributors.

Pattern examples are adapted from
[refactoring.guru](https://refactoring.guru/design-patterns/python) 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.