patchwork-conventions 0.1.1__tar.gz → 0.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/PKG-INFO +30 -4
  2. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/README.md +29 -3
  3. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/pyproject.toml +1 -1
  4. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/cli.py +6 -6
  5. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/mcp/server.py +37 -5
  6. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/miners/api_patterns.py +35 -20
  7. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/miners/config_detector.py +28 -7
  8. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/miners/git_patterns.py +2 -2
  9. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork_conventions.egg-info/PKG-INFO +30 -4
  10. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/LICENSE +0 -0
  11. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/setup.cfg +0 -0
  12. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/__init__.py +0 -0
  13. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/mcp/__init__.py +0 -0
  14. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/miners/__init__.py +0 -0
  15. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/miners/ast_base.py +0 -0
  16. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/miners/error_handling.py +0 -0
  17. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/miners/imports.py +0 -0
  18. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/miners/naming.py +0 -0
  19. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/miners/structure.py +0 -0
  20. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/miners/testing.py +0 -0
  21. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/output/__init__.py +0 -0
  22. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/output/report.py +0 -0
  23. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/scanner.py +0 -0
  24. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork_conventions.egg-info/SOURCES.txt +0 -0
  25. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork_conventions.egg-info/dependency_links.txt +0 -0
  26. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork_conventions.egg-info/entry_points.txt +0 -0
  27. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork_conventions.egg-info/requires.txt +0 -0
  28. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork_conventions.egg-info/top_level.txt +0 -0
  29. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/tests/test_naming.py +0 -0
  30. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/tests/test_scanner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchwork-conventions
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Mine your codebase. Generate CONVENTIONS.md. Stop AI agents from making up your style.
5
5
  Author: patchwork contributors
6
6
  License: MIT
@@ -228,20 +228,46 @@ patchwork scan --claude-md
228
228
 
229
229
  ### Option 3: MCP server
230
230
 
231
- Add to `~/.claude/settings.json`:
231
+ **Claude Code** — add to `~/.claude.json` (or run `claude mcp add` interactively):
232
232
 
233
233
  ```json
234
234
  {
235
235
  "mcpServers": {
236
236
  "patchwork": {
237
237
  "command": "patchwork",
238
- "args": ["serve", "--stdio", "/path/to/your/project"]
238
+ "args": ["serve", "/path/to/your/project", "--stdio"]
239
239
  }
240
240
  }
241
241
  }
242
242
  ```
243
243
 
244
- Then Claude Code can use 8 on-demand tools:
244
+ **Claude Desktop** add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
245
+
246
+ ```json
247
+ {
248
+ "mcpServers": {
249
+ "patchwork": {
250
+ "command": "patchwork",
251
+ "args": ["serve", "/path/to/your/project", "--stdio"]
252
+ }
253
+ }
254
+ }
255
+ ```
256
+
257
+ **Cursor** — add to `.cursor/mcp.json` in your project root:
258
+
259
+ ```json
260
+ {
261
+ "mcpServers": {
262
+ "patchwork": {
263
+ "command": "patchwork",
264
+ "args": ["serve", ".", "--stdio"]
265
+ }
266
+ }
267
+ }
268
+ ```
269
+
270
+ Then your AI agent can use 8 on-demand tools:
245
271
 
246
272
  | Tool | When to use |
247
273
  |---|---|
@@ -183,20 +183,46 @@ patchwork scan --claude-md
183
183
 
184
184
  ### Option 3: MCP server
185
185
 
186
- Add to `~/.claude/settings.json`:
186
+ **Claude Code** — add to `~/.claude.json` (or run `claude mcp add` interactively):
187
187
 
188
188
  ```json
189
189
  {
190
190
  "mcpServers": {
191
191
  "patchwork": {
192
192
  "command": "patchwork",
193
- "args": ["serve", "--stdio", "/path/to/your/project"]
193
+ "args": ["serve", "/path/to/your/project", "--stdio"]
194
194
  }
195
195
  }
196
196
  }
197
197
  ```
198
198
 
199
- Then Claude Code can use 8 on-demand tools:
199
+ **Claude Desktop** add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
200
+
201
+ ```json
202
+ {
203
+ "mcpServers": {
204
+ "patchwork": {
205
+ "command": "patchwork",
206
+ "args": ["serve", "/path/to/your/project", "--stdio"]
207
+ }
208
+ }
209
+ }
210
+ ```
211
+
212
+ **Cursor** — add to `.cursor/mcp.json` in your project root:
213
+
214
+ ```json
215
+ {
216
+ "mcpServers": {
217
+ "patchwork": {
218
+ "command": "patchwork",
219
+ "args": ["serve", ".", "--stdio"]
220
+ }
221
+ }
222
+ }
223
+ ```
224
+
225
+ Then your AI agent can use 8 on-demand tools:
200
226
 
201
227
  | Tool | When to use |
202
228
  |---|---|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "patchwork-conventions"
7
- version = "0.1.1"
7
+ version = "0.1.3"
8
8
  description = "Mine your codebase. Generate CONVENTIONS.md. Stop AI agents from making up your style."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -111,7 +111,7 @@ def scan(
111
111
 
112
112
  if claude_md and out_path.exists():
113
113
  # Append patchwork section to existing CLAUDE.md
114
- existing = out_path.read_text()
114
+ existing = out_path.read_text(encoding="utf-8")
115
115
  marker = "<!-- patchwork:start -->"
116
116
  end_marker = "<!-- patchwork:end -->"
117
117
  if marker in existing:
@@ -126,7 +126,7 @@ def scan(
126
126
  else:
127
127
  text = existing.rstrip() + f"\n\n{marker}\n{text}\n{end_marker}\n"
128
128
 
129
- out_path.write_text(text)
129
+ out_path.write_text(text, encoding="utf-8")
130
130
 
131
131
  if not quiet:
132
132
  _print_summary(report, out_path)
@@ -145,7 +145,7 @@ def update(path: str, output: str | None) -> None:
145
145
  # Load any existing manual annotations
146
146
  manual_sections: dict[str, str] = {}
147
147
  if out_path.exists():
148
- manual_sections = _extract_manual_sections(out_path.read_text())
148
+ manual_sections = _extract_manual_sections(out_path.read_text(encoding="utf-8"))
149
149
 
150
150
  opts = ScanOptions(root=root)
151
151
  with Progress(SpinnerColumn(), TextColumn("{task.description}"), transient=True) as p:
@@ -158,7 +158,7 @@ def update(path: str, output: str | None) -> None:
158
158
  for heading, content in manual_sections.items():
159
159
  text += f"\n\n## {heading} (manual)\n\n{content}"
160
160
 
161
- out_path.write_text(text)
161
+ out_path.write_text(text, encoding="utf-8")
162
162
  console.print(f"[green]✓[/green] Updated [bold]{out_path}[/bold]")
163
163
  if manual_sections:
164
164
  console.print(f" Preserved {len(manual_sections)} manual section(s)")
@@ -182,7 +182,7 @@ def diff(path: str) -> None:
182
182
  console.print("[yellow]No existing CONVENTIONS.md — run `patchwork scan` first[/yellow]")
183
183
  sys.exit(1)
184
184
 
185
- old_text = out_path.read_text()
185
+ old_text = out_path.read_text(encoding="utf-8")
186
186
  diffs = list(difflib.unified_diff(
187
187
  old_text.splitlines(),
188
188
  new_text.splitlines(),
@@ -241,7 +241,7 @@ def watch(path: str, interval: float) -> None:
241
241
  if changed:
242
242
  last_mtime = current
243
243
  report = do_scan(opts)
244
- out_path.write_text(report.to_markdown())
244
+ out_path.write_text(report.to_markdown(), encoding="utf-8")
245
245
  console.print(f"[green]↺[/green] CONVENTIONS.md updated")
246
246
  except KeyboardInterrupt:
247
247
  console.print("\n[dim]Stopped[/dim]")
@@ -36,7 +36,7 @@ def _get_or_scan(root: Path) -> ConventionReport:
36
36
  cached = _CACHE.get(key)
37
37
  if cached is not None:
38
38
  return cached
39
- opts = ScanOptions(root=root, max_files=300)
39
+ opts = ScanOptions(root=root, max_files=500)
40
40
  report = do_scan(opts)
41
41
  _CACHE[key] = report
42
42
  return report
@@ -189,9 +189,34 @@ async def run_server(root: Path, port: int = 3742, stdio: bool = True) -> None:
189
189
  ),
190
190
  ]
191
191
 
192
+ def _resolve_scan_root(raw_path: str | None) -> Path:
193
+ """Resolve and validate the scan path.
194
+
195
+ Rejects paths that escape the filesystem root or point to sensitive
196
+ system directories, and normalises symlinks so containment checks
197
+ are reliable. The MCP server is a read-only tool, but we still
198
+ validate inputs at the boundary to follow least-privilege.
199
+ """
200
+ candidate = Path(raw_path).resolve() if raw_path else root.resolve()
201
+
202
+ # Reject obviously dangerous system paths
203
+ _BLOCKED = {"/etc", "/proc", "/sys", "/dev", "/private/etc"}
204
+ for blocked in _BLOCKED:
205
+ if str(candidate).startswith(blocked):
206
+ raise ValueError(f"Scanning system path '{candidate}' is not allowed.")
207
+
208
+ # Must be an existing directory (not a file or a socket etc.)
209
+ if not candidate.is_dir():
210
+ raise ValueError(f"Path '{candidate}' is not a directory.")
211
+
212
+ return candidate
213
+
192
214
  @server.call_tool()
193
215
  async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
194
- scan_root = Path(arguments.get("path", str(root))).resolve()
216
+ try:
217
+ scan_root = _resolve_scan_root(arguments.get("path"))
218
+ except ValueError as exc:
219
+ return [types.TextContent(type="text", text=f"Error: {exc}")]
195
220
 
196
221
  if arguments.get("refresh"):
197
222
  _invalidate(scan_root)
@@ -305,6 +330,8 @@ async def run_server(root: Path, port: int = 3742, stdio: bool = True) -> None:
305
330
  lines.append(f" logging: {er.logging_framework}")
306
331
  if er.propagation_style:
307
332
  lines.append(f" propagation: {er.propagation_style}")
333
+ if er.custom_exceptions:
334
+ lines.append(f" custom exceptions: {', '.join(er.custom_exceptions[:6])}")
308
335
  for note in er.notes:
309
336
  lines.append(f" note: {note}")
310
337
  return [types.TextContent(type="text", text="\n".join(lines) or "No error patterns found.")]
@@ -360,8 +387,13 @@ async def run_server(root: Path, port: int = 3742, stdio: bool = True) -> None:
360
387
  return [types.TextContent(type="text", text="\n".join(lines))]
361
388
 
362
389
  elif name == "patchwork_check":
363
- sym = arguments["name"]
364
- kind = arguments["kind"]
390
+ sym = arguments.get("name")
391
+ kind = arguments.get("kind")
392
+ if not sym or not kind:
393
+ return [types.TextContent(
394
+ type="text",
395
+ text="Error: 'name' and 'kind' are required arguments.",
396
+ )]
365
397
  lang = arguments.get("language", "")
366
398
  return [types.TextContent(
367
399
  type="text",
@@ -391,7 +423,7 @@ async def run_server(root: Path, port: int = 3742, stdio: bool = True) -> None:
391
423
  Route("/sse", endpoint=handle_sse),
392
424
  Mount("/messages", app=sse.handle_post_message),
393
425
  ])
394
- uvicorn.run(app, host="0.0.0.0", port=port)
426
+ uvicorn.run(app, host="127.0.0.1", port=port)
395
427
 
396
428
 
397
429
  def _check_convention(name: str, kind: str, lang: str, report: ConventionReport) -> str:
@@ -34,10 +34,23 @@ _RESP_DATA_ERROR = re.compile(r'["\'](?:data|error)["\']', re.IGNORECASE)
34
34
  _RESP_SUCCESS_DATA = re.compile(r'["\']success["\'].*["\']data["\']', re.DOTALL | re.IGNORECASE)
35
35
  _RESP_RESULT = re.compile(r'["\']result["\']', re.IGNORECASE)
36
36
 
37
- # Route parameter styles
38
- _ROUTE_COLON = re.compile(r'(?:app|router)\.\w+\(["\'][^"\']*:\w+') # Express :id
39
- _ROUTE_BRACE = re.compile(r'(?:path|url)\s*=\s*["\'][^"\']*\{[a-zA-Z_]+\}') # FastAPI {id}
40
- _ROUTE_ANGLE = re.compile(r'(?:app|blueprint)\.\w+\(["\'][^"\']*<[a-zA-Z_:]+>') # Flask <id>
37
+ # Route parameter styles — match route strings passed to known route-registration calls.
38
+ # We look for the route string ONLY when it appears as an argument to a route decorator
39
+ # or router method, to avoid matching f-strings, format strings, and other /path usages.
40
+
41
+ # FastAPI/Starlette: @app.get("/items/{item_id}") or @router.post("/x/{id:int}")
42
+ _ROUTE_BRACE = re.compile(
43
+ r'(?:@?\w+\.(?:get|post|put|patch|delete|websocket|route|add_api_route)\s*\()'
44
+ r'\s*[f]?["\'][^"\']*\{[a-zA-Z_]\w*(?::[a-zA-Z]+)?\}'
45
+ )
46
+ # Flask: @app.route("/items/<int:item_id>")
47
+ _ROUTE_ANGLE = re.compile(
48
+ r'(?:@?\w+\.route\s*\()\s*[f]?["\'][^"\']*<(?:[a-zA-Z_]+:)?[a-zA-Z_]\w*>'
49
+ )
50
+ # Express: router.get("/:id") or app.use("/items/:id")
51
+ _ROUTE_COLON = re.compile(
52
+ r'(?:\w+\.(?:get|post|put|patch|delete|use|all)\s*\()\s*["\'][^"\']*(?<!\{):(?!\w*\})[a-zA-Z_]\w*'
53
+ )
41
54
 
42
55
  # ORM signals
43
56
  _ORM_SIGNALS = {
@@ -57,7 +70,8 @@ _FRAMEWORK_SIGNALS = {
57
70
  "FastAPI": [r"from fastapi import", r"@app\.get\(", r"@router\."],
58
71
  "Flask": [r"from flask import", r"@app\.route\(", r"Blueprint\("],
59
72
  "Django": [r"from django", r"urlpatterns\s*=", r"HttpResponse"],
60
- "Express": [r"require\(['\"]express['\"]", r"app\.use\(", r"router\.get\("],
73
+ # For Express: require both the require() AND usage — router.get alone is too generic
74
+ "Express": [r"require\(['\"]express['\"]\)", r"express\(\)"],
61
75
  "Fastify": [r"require\(['\"]fastify['\"]", r"fastify\.register"],
62
76
  "Hono": [r"from ['\"]hono['\"]", r"new Hono\("],
63
77
  "Gin": [r"\bgin\b.*\bDefault\(\)", r"r\.GET\(", r"c\.JSON\("],
@@ -105,9 +119,10 @@ def _detect_apis(paths: list[Path], lang: str) -> APIResult:
105
119
  data_error = 0
106
120
  success_data = 0
107
121
  result_shape = 0
108
- colon_routes = 0
109
- brace_routes = 0
110
- angle_routes = 0
122
+ # Track route style per FILE (not per match) to avoid single-file anomalies
123
+ colon_route_files = 0
124
+ brace_route_files = 0
125
+ angle_route_files = 0
111
126
  # Track per-file hits (not per-match) to avoid false positives from
112
127
  # code that merely mentions framework names in strings, comments, or docs.
113
128
  orm_files: Counter[str] = Counter()
@@ -127,9 +142,9 @@ def _detect_apis(paths: list[Path], lang: str) -> APIResult:
127
142
  success_data += len(_RESP_SUCCESS_DATA.findall(text))
128
143
  result_shape += len(_RESP_RESULT.findall(text))
129
144
 
130
- colon_routes += len(_ROUTE_COLON.findall(text))
131
- brace_routes += len(_ROUTE_BRACE.findall(text))
132
- angle_routes += len(_ROUTE_ANGLE.findall(text))
145
+ if _ROUTE_COLON.search(text): colon_route_files += 1
146
+ if _ROUTE_BRACE.search(text): brace_route_files += 1
147
+ if _ROUTE_ANGLE.search(text): angle_route_files += 1
133
148
 
134
149
  # Skip files that are themselves pattern-definition files (e.g. this miner)
135
150
  # to avoid self-matching on our own regex strings.
@@ -175,17 +190,17 @@ def _detect_apis(paths: list[Path], lang: str) -> APIResult:
175
190
  elif result_shape > 3:
176
191
  response_shape = "{result}"
177
192
 
178
- # Route param style
179
- route_total = colon_routes + brace_routes + angle_routes
193
+ # Route param style — pick the style seen in the most files.
194
+ # Require at least 2 files to avoid single-file anomalies.
180
195
  route_style = None
181
- if route_total > 0:
182
- best = max(colon_routes, brace_routes, angle_routes)
183
- if colon_routes == best:
184
- route_style = ":id (Express style)"
185
- elif brace_routes == best:
186
- route_style = "{id} (FastAPI style)"
187
- else:
196
+ best = max(brace_route_files, angle_route_files, colon_route_files)
197
+ if best >= 2:
198
+ if brace_route_files == best:
199
+ route_style = "{id} (FastAPI/Starlette style)"
200
+ elif angle_route_files == best:
188
201
  route_style = "<id> (Flask style)"
202
+ else:
203
+ route_style = ":id (Express style)"
189
204
 
190
205
  return APIResult(
191
206
  response_shape=response_shape,
@@ -17,6 +17,19 @@ except ImportError:
17
17
  except ImportError:
18
18
  tomllib = None # type: ignore
19
19
 
20
+ # Hard limit on config file size to prevent DoS from pathologically large files
21
+ _MAX_CONFIG_BYTES = 1 * 1024 * 1024 # 1 MB
22
+
23
+
24
+ def _safe_read(path: Path) -> str | None:
25
+ """Read a config file, returning None if it exceeds the size limit."""
26
+ try:
27
+ if path.stat().st_size > _MAX_CONFIG_BYTES:
28
+ return None
29
+ return path.read_text(encoding="utf-8", errors="replace")
30
+ except OSError:
31
+ return None
32
+
20
33
 
21
34
  @dataclass
22
35
  class ProjectConfig:
@@ -129,9 +142,12 @@ class ConfigDetector:
129
142
  return cfg
130
143
 
131
144
  def _read_package_json(self, path: Path, cfg: ProjectConfig) -> None:
145
+ text = _safe_read(path)
146
+ if text is None:
147
+ return
132
148
  try:
133
- data = json.loads(path.read_text())
134
- except (json.JSONDecodeError, OSError):
149
+ data = json.loads(text)
150
+ except json.JSONDecodeError:
135
151
  return
136
152
 
137
153
  cfg.language = "javascript/typescript"
@@ -179,8 +195,11 @@ class ConfigDetector:
179
195
  cfg.language = "python"
180
196
  if tomllib is None:
181
197
  return
198
+ text = _safe_read(path)
199
+ if text is None:
200
+ return
182
201
  try:
183
- data = tomllib.loads(path.read_text())
202
+ data = tomllib.loads(text)
184
203
  except Exception:
185
204
  return
186
205
 
@@ -225,9 +244,8 @@ class ConfigDetector:
225
244
  def _read_go_mod(self, path: Path, cfg: ProjectConfig) -> None:
226
245
  cfg.language = "go"
227
246
  cfg.package_manager = "go"
228
- try:
229
- text = path.read_text()
230
- except OSError:
247
+ text = _safe_read(path)
248
+ if text is None:
231
249
  return
232
250
  m = re.search(r'^module\s+(\S+)', text, re.MULTILINE)
233
251
  if m:
@@ -253,8 +271,11 @@ class ConfigDetector:
253
271
  cfg.package_manager = "cargo"
254
272
  if tomllib is None:
255
273
  return
274
+ text = _safe_read(path)
275
+ if text is None:
276
+ return
256
277
  try:
257
- data = tomllib.loads(path.read_text())
278
+ data = tomllib.loads(text)
258
279
  except Exception:
259
280
  return
260
281
  pkg = data.get("package", {})
@@ -9,7 +9,7 @@ GitPatternMiner — Mines git history for workflow conventions:
9
9
  from __future__ import annotations
10
10
 
11
11
  import re
12
- import subprocess
12
+ import subprocess # nosec B404 — used only for 'git' with a hardcoded arg list, no shell=True
13
13
  from collections import Counter, defaultdict
14
14
  from dataclasses import dataclass, field
15
15
  from pathlib import Path
@@ -40,7 +40,7 @@ _BRANCH_FIX = re.compile(r'^(fix|hotfix|bugfix)/[\w/-]+$')
40
40
 
41
41
  def _run_git(args: list[str], cwd: Path, max_bytes: int = 500_000) -> str:
42
42
  try:
43
- result = subprocess.run(
43
+ result = subprocess.run( # nosec B603 — list args, no shell=True, cwd is a validated Path
44
44
  ["git"] + args,
45
45
  cwd=str(cwd),
46
46
  capture_output=True,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchwork-conventions
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Mine your codebase. Generate CONVENTIONS.md. Stop AI agents from making up your style.
5
5
  Author: patchwork contributors
6
6
  License: MIT
@@ -228,20 +228,46 @@ patchwork scan --claude-md
228
228
 
229
229
  ### Option 3: MCP server
230
230
 
231
- Add to `~/.claude/settings.json`:
231
+ **Claude Code** — add to `~/.claude.json` (or run `claude mcp add` interactively):
232
232
 
233
233
  ```json
234
234
  {
235
235
  "mcpServers": {
236
236
  "patchwork": {
237
237
  "command": "patchwork",
238
- "args": ["serve", "--stdio", "/path/to/your/project"]
238
+ "args": ["serve", "/path/to/your/project", "--stdio"]
239
239
  }
240
240
  }
241
241
  }
242
242
  ```
243
243
 
244
- Then Claude Code can use 8 on-demand tools:
244
+ **Claude Desktop** add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
245
+
246
+ ```json
247
+ {
248
+ "mcpServers": {
249
+ "patchwork": {
250
+ "command": "patchwork",
251
+ "args": ["serve", "/path/to/your/project", "--stdio"]
252
+ }
253
+ }
254
+ }
255
+ ```
256
+
257
+ **Cursor** — add to `.cursor/mcp.json` in your project root:
258
+
259
+ ```json
260
+ {
261
+ "mcpServers": {
262
+ "patchwork": {
263
+ "command": "patchwork",
264
+ "args": ["serve", ".", "--stdio"]
265
+ }
266
+ }
267
+ }
268
+ ```
269
+
270
+ Then your AI agent can use 8 on-demand tools:
245
271
 
246
272
  | Tool | When to use |
247
273
  |---|---|