Option C Tools
Loading...
Searching...
No Matches
tools.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oct/mcp/tools.py
4
5"""
6Purpose
7-------
8Phase 5A + 5B tool manifest — 20 MCP tools (9 read-only + 11 write)
9registered with the FastMCP server. Each tool function wires the
10validated input through the six-layer pipeline: validator → policy →
11executor → redactor → audit logger. Write tools add a HITL safety
12gate step between policy and execution.
13
14Responsibilities
15----------------
16- Define :func:`register_tools` which attaches all 9 Phase 5A tools.
17- Define :func:`register_write_tools` which attaches all 11 Phase 5B
18 write tools.
19- Each read tool function follows the six-layer pipeline.
20- Each write tool function follows the same pipeline but calls
21 :func:`_run_tool_write` which inserts the safety gate (Layer 2.5)
22 between policy and execution.
23
24Write tool pipeline (HITL gate)
25--------------------------------
26 rate-limiter → policy → validator → **safety gate** → executor →
27 redactor → audit
28
29Diagnostics
30-----------
31Domain: MCP
32Levels:
33 L2 — lifecycle: tool called, tool returned
34 L3 — request details
35 L4 — deep trace
36
37Contracts
38---------
39- Tool functions never raise for expected error conditions — they return
40 a structured error string that the MCP client sees as tool output.
41- The :func:`register_tools` function is idempotent for a given
42 FastMCP instance.
43- Tool functions always write one audit record per invocation, even on
44 policy denial or validation failure.
45"""
46
47from __future__ import annotations
48
49import getpass
50import hashlib
51import time
52import uuid
53from pathlib import Path
54from typing import Any
55
56try:
57 from mcp.server.fastmcp import FastMCP
58except ImportError as _mcp_err: # pragma: no cover
59 raise ImportError(
60 "mcp>=1.0 is required for oct-mcp. "
61 "Install with: pip install 'oct[mcp]'"
62 ) from _mcp_err
63
64try:
65 from oc_diagnostics import _dbg as _real_dbg
66
67 def _dbg(*args, **kwargs) -> None:
68 _real_dbg(*args, **kwargs)
69except ImportError: # pragma: no cover
70 def _dbg(*args, **kwargs) -> None:
71 return None
72
73from oct.mcp.audit import McpAuditRecord, _hash_project_root, _utc_now_iso
74from oct.mcp.safety import SafetyDecision, _format_confirmation_request
75from oct.mcp.server import get_server_state
76from oct.mcp.validator import validate_tool_args
77
78
79# ---------------------------------------------------------------------
80# Shared pipeline runner
81# ---------------------------------------------------------------------
82
83
84def _run_tool(tool_name: str, raw_args: dict) -> str:
85 """Execute one tool through the full six-layer pipeline.
86
87 This is the single shared entry point called by every tool function.
88 It handles all error conditions and always writes an audit record.
89
90 Parameters
91 ----------
92 tool_name
93 MCP tool name, e.g. ``"oct_lint"``.
94 raw_args
95 Unvalidated dict from the MCP client.
96
97 Returns
98 -------
99 Redacted tool output string (or a structured error message).
100 """
101 func = "_run_tool"
102 state = get_server_state()
103 request_id = str(uuid.uuid4())
104 start_time = time.monotonic()
105
106 _dbg("MCP", func, f"tool={tool_name} request_id={request_id}", 2)
107
108 # Build a skeleton audit record filled in as we proceed.
109 record = McpAuditRecord(
110 timestamp=_utc_now_iso(),
111 session_id=state.session_id,
112 request_id=request_id,
113 tool=tool_name,
114 args={}, # filled after validation
115 args_redacted=True,
116 user=_safe_getuser(),
117 project_root_hash=_hash_project_root(state.project_root),
118 )
119
120 try:
121 # Layer 1: rate limiter
122 if not state.rate_limiter.consume():
123 record.decision = "denied"
124 record.policy_rule = "rate_limit"
125 record.duration_ms = _elapsed_ms(start_time)
126 state.audit_logger.write(record)
127 state.metrics.record_rate_limit_hit(tool_name)
128 return _error_response(
129 "Rate limit exceeded. Please wait before making more tool calls.",
130 tool_name,
131 )
132
133 # Layer 3: policy check (before validation, so unknown tools are rejected fast)
134 decision = state.policy.check(tool_name)
135 record.decision = decision.decision
136 record.policy_rule = decision.policy_rule
137
138 if not decision.allowed:
139 record.duration_ms = _elapsed_ms(start_time)
140 state.audit_logger.write(record)
141 state.metrics.record_call(tool_name, decision.decision,
142 _elapsed_ms(start_time) / 1000, 0)
143 return _error_response(decision.reason, tool_name)
144
145 # Layer 2: input validation (Pydantic)
146 try:
147 validated_model = validate_tool_args(
148 tool_name, raw_args, project_root=state.project_root
149 )
150 except Exception as exc:
151 record.decision = "denied"
152 record.policy_rule = "validation_failure"
153 record.duration_ms = _elapsed_ms(start_time)
154 state.audit_logger.write(record)
155 state.metrics.record_validation_failure(tool_name)
156 return _error_response(f"Input validation failed: {exc}", tool_name)
157
158 # Store redacted args in audit record.
159 record.args = validated_model.model_dump()
160
161 # Layer 4: execution sandbox
162 sandbox_result = state.executor.execute(
163 tool_name=tool_name,
164 validated_args=validated_model.model_dump(),
165 project_root=state.project_root,
166 )
167
168 raw_output = sandbox_result.stdout
169 if sandbox_result.stderr:
170 if raw_output:
171 raw_output += "\n--- stderr ---\n" + sandbox_result.stderr
172 else:
173 raw_output = sandbox_result.stderr
174
175 # Compute output hash before redaction (integrity check).
176 output_hash = hashlib.sha256(
177 raw_output.encode("utf-8", errors="replace")
178 ).hexdigest()
179
180 # Layer 5: output redaction
181 redactor_result = state.redactor.apply_all(raw_output, state.project_root)
182
183 # Layer 6: audit record completion
184 record.exit_code = sandbox_result.exit_code
185 duration_ms = _elapsed_ms(start_time)
186 record.duration_ms = duration_ms
187 record.output_size_bytes = len(
188 redactor_result.text.encode("utf-8", errors="replace")
189 )
190 record.output_truncated = (
191 sandbox_result.output_truncated or redactor_result.truncated
192 )
193 record.output_hash_sha256 = output_hash
194 record.redactions_applied = redactor_result.redactions_applied
195
196 state.audit_logger.write(record)
197 state.metrics.record_call(
198 tool_name, "allowed", duration_ms / 1000, redactor_result.redactions_applied
199 )
200
201 _dbg(
202 "MCP", func,
203 f"tool={tool_name} exit_code={sandbox_result.exit_code} "
204 f"redactions={redactor_result.redactions_applied}",
205 2,
206 )
207 return redactor_result.text
208
209 except Exception as exc: # pragma: no cover - catch-all for unexpected errors
210 _dbg("MCP", func, f"SYSTEM_ERROR: unexpected error in {tool_name}: {exc}", 1)
211 record.decision = "denied"
212 record.policy_rule = "internal_error"
213 record.duration_ms = _elapsed_ms(start_time)
214 try:
215 state.audit_logger.write(record)
216 except Exception:
217 pass
218 return _error_response(f"Internal server error: {exc}", tool_name)
219
220
221def _run_tool_write(tool_name: str, raw_args: dict) -> str:
222 """Execute one write tool through the HITL-extended pipeline.
223
224 Pipeline: rate-limiter → policy → validator → safety gate →
225 executor → redactor → audit.
226
227 When the safety gate requires confirmation (``confirm=False`` in
228 args), returns a structured ``[oct-mcp requires_confirmation] …``
229 string without executing anything. The AI host must re-submit with
230 ``confirm=True`` after the user approves.
231
232 Parameters
233 ----------
234 tool_name
235 One of the 11 Phase 5B write tool names.
236 raw_args
237 Unvalidated dict from the MCP client.
238
239 Returns
240 -------
241 Redacted tool output string, a confirmation-required string, or a
242 structured error message.
243 """
244 func = "_run_tool_write"
245 state = get_server_state()
246 request_id = str(uuid.uuid4())
247 start_time = time.monotonic()
248
249 _dbg("MCP", func, f"tool={tool_name} request_id={request_id}", 2)
250
251 from oct.mcp.policy import TOOL_MANIFEST
252
253 record = McpAuditRecord(
254 timestamp=_utc_now_iso(),
255 session_id=state.session_id,
256 request_id=request_id,
257 tool=tool_name,
258 args={},
259 args_redacted=True,
260 user=_safe_getuser(),
261 project_root_hash=_hash_project_root(state.project_root),
262 )
263
264 try:
265 # Layer 1: rate limiter
266 if not state.rate_limiter.consume():
267 record.decision = "denied"
268 record.policy_rule = "rate_limit"
269 record.duration_ms = _elapsed_ms(start_time)
270 state.audit_logger.write(record)
271 state.metrics.record_rate_limit_hit(tool_name)
272 return _error_response(
273 "Rate limit exceeded. Please wait before making more tool calls.",
274 tool_name,
275 )
276
277 # Layer 3: policy check
278 decision = state.policy.check(tool_name)
279 record.decision = decision.decision
280 record.policy_rule = decision.policy_rule
281
282 if not decision.allowed:
283 record.duration_ms = _elapsed_ms(start_time)
284 state.audit_logger.write(record)
285 state.metrics.record_call(tool_name, decision.decision,
286 _elapsed_ms(start_time) / 1000, 0)
287 return _error_response(decision.reason, tool_name)
288
289 # Layer 2: input validation (Pydantic)
290 try:
291 validated_model = validate_tool_args(
292 tool_name, raw_args, project_root=state.project_root
293 )
294 except Exception as exc:
295 record.decision = "denied"
296 record.policy_rule = "validation_failure"
297 record.duration_ms = _elapsed_ms(start_time)
298 state.audit_logger.write(record)
299 state.metrics.record_validation_failure(tool_name)
300 return _error_response(f"Input validation failed: {exc}", tool_name)
301
302 record.args = validated_model.model_dump()
303
304 # Layer 2.5: HITL safety gate
305 spec = TOOL_MANIFEST.get(tool_name)
306 if spec is None: # pragma: no cover — policy already checked this
307 record.decision = "denied"
308 record.policy_rule = "unknown_tool"
309 record.duration_ms = _elapsed_ms(start_time)
310 state.audit_logger.write(record)
311 return _error_response(f"Unknown tool: {tool_name}", tool_name)
312
313 safety_decision: SafetyDecision = state.safety.check(
314 tool_name, spec, validated_model.model_dump()
315 )
316
317 if not safety_decision.proceed:
318 record.decision = "requires_confirmation"
319 record.policy_rule = "hitl_gate"
320 duration_ms = _elapsed_ms(start_time)
321 record.duration_ms = duration_ms
322 state.audit_logger.write(record)
323 state.metrics.record_call(tool_name, "requires_confirmation",
324 duration_ms / 1000, 0)
325 return _format_confirmation_request(
326 tool_name, spec, validated_model.model_dump()
327 )
328
329 # Layer 4: execution sandbox
330 sandbox_result = state.executor.execute_write(
331 tool_name=tool_name,
332 validated_args=validated_model.model_dump(),
333 project_root=state.project_root,
334 dry_run_override=safety_decision.dry_run,
335 )
336
337 raw_output = sandbox_result.stdout
338 if sandbox_result.stderr:
339 if raw_output:
340 raw_output += "\n--- stderr ---\n" + sandbox_result.stderr
341 else:
342 raw_output = sandbox_result.stderr
343
344 output_hash = hashlib.sha256(
345 raw_output.encode("utf-8", errors="replace")
346 ).hexdigest()
347
348 # Layer 5: output redaction
349 redactor_result = state.redactor.apply_all(raw_output, state.project_root)
350
351 # Layer 6: audit record completion
352 record.decision = "allowed"
353 record.exit_code = sandbox_result.exit_code
354 duration_ms = _elapsed_ms(start_time)
355 record.duration_ms = duration_ms
356 record.output_size_bytes = len(
357 redactor_result.text.encode("utf-8", errors="replace")
358 )
359 record.output_truncated = (
360 sandbox_result.output_truncated or redactor_result.truncated
361 )
362 record.output_hash_sha256 = output_hash
363 record.redactions_applied = redactor_result.redactions_applied
364
365 state.audit_logger.write(record)
366 state.metrics.record_call(
367 tool_name, "allowed", duration_ms / 1000, redactor_result.redactions_applied
368 )
369
370 _dbg(
371 "MCP", func,
372 f"tool={tool_name} exit_code={sandbox_result.exit_code} "
373 f"redactions={redactor_result.redactions_applied}",
374 2,
375 )
376 return redactor_result.text
377
378 except Exception as exc: # pragma: no cover - catch-all for unexpected errors
379 _dbg("MCP", func, f"SYSTEM_ERROR: unexpected error in {tool_name}: {exc}", 1)
380 record.decision = "denied"
381 record.policy_rule = "internal_error"
382 record.duration_ms = _elapsed_ms(start_time)
383 try:
384 state.audit_logger.write(record)
385 except Exception:
386 pass
387 return _error_response(f"Internal server error: {exc}", tool_name)
388
389
390def _elapsed_ms(start: float) -> int:
391 return int((time.monotonic() - start) * 1000)
392
393
394def _safe_getuser() -> str:
395 try:
396 return getpass.getuser()
397 except Exception:
398 return ""
399
400
401def _error_response(message: str, tool_name: str) -> str:
402 """Return a structured error string visible to the MCP client."""
403 return f"[oct-mcp error] {tool_name}: {message}"
404
405
406# ---------------------------------------------------------------------
407# Tool registration
408# ---------------------------------------------------------------------
409
410
411def register_tools(mcp: FastMCP) -> None:
412 """Register all 9 Phase 5A tools with the FastMCP server instance.
413
414 Called once during :func:`~oct.mcp.server.start_server`.
415 """
416
417 @mcp.tool(
418 name="oct_lint",
419 description=(
420 "Run the Option C linter on the project. Returns a JSON summary "
421 "of lint violations including header checks, secret detection, "
422 "and coding style enforcement. Use `changed=true` to lint only "
423 "git-modified files."
424 ),
425 )
426 def oct_lint(
427 paths: list[str] | None = None,
428 json_output: bool = True,
429 jobs: int = 1,
430 changed: bool = False,
431 fix_headers: bool = False,
432 profile: str | None = None,
433 ) -> str:
434 args: dict = {
435 "paths": paths or [],
436 "json_output": json_output,
437 "jobs": jobs,
438 "changed": changed,
439 "fix_headers": fix_headers,
440 "profile": profile,
441 }
442 return _run_tool("oct_lint", args)
443
444 @mcp.tool(
445 name="oct_health",
446 description=(
447 "Run the Option C project health check. Returns a summary of "
448 "project structure integrity, configuration validity, and any "
449 "detected issues."
450 ),
451 )
452 def oct_health(
453 json_output: bool = True,
454 verbose: bool = False,
455 ) -> str:
456 return _run_tool("oct_health", {"json_output": json_output, "verbose": verbose})
457
458 @mcp.tool(
459 name="oct_skeleton",
460 description=(
461 "Export the project source skeleton (file tree with content "
462 "stubs). Returns a JSON manifest of exported files. Sensitive "
463 "files (.env, *.key, etc.) are automatically excluded."
464 ),
465 )
466 def oct_skeleton(
467 json_output: bool = True,
468 output_dir: str | None = None,
469 ) -> str:
470 return _run_tool("oct_skeleton", {"json_output": json_output, "output_dir": output_dir})
471
472 @mcp.tool(
473 name="oct_deps",
474 description=(
475 "Inspect project dependencies. Returns a JSON summary of "
476 "installed packages, missing dependencies, and version mismatches."
477 ),
478 )
479 def oct_deps(
480 json_output: bool = True,
481 check: bool = False,
482 ) -> str:
483 return _run_tool("oct_deps", {"json_output": json_output, "check": check})
484
485 @mcp.tool(
486 name="oct_test",
487 description=(
488 "Run the project test suite using pytest. Returns a JSON summary "
489 "of test results including pass/fail counts and short failure "
490 "tracebacks. Use `changed=true` to run only tests related to "
491 "git-modified files."
492 ),
493 )
494 def oct_test(
495 json_output: bool = True,
496 paths: list[str] | None = None,
497 changed: bool = False,
498 jobs: int = 1,
499 ) -> str:
500 args: dict = {
501 "json_output": json_output,
502 "paths": paths or [],
503 "changed": changed,
504 "jobs": jobs,
505 }
506 return _run_tool("oct_test", args)
507
508 @mcp.tool(
509 name="oct_typecheck",
510 description=(
511 "Run type checking on the project using pyright (falls back to "
512 "mypy if pyright is not installed). Returns a JSON object with "
513 "the tool used, output, and exit code."
514 ),
515 )
516 def oct_typecheck(
517 json_output: bool = True,
518 paths: list[str] | None = None,
519 ) -> str:
520 args: dict = {
521 "json_output": json_output,
522 "paths": paths or [],
523 }
524 return _run_tool("oct_typecheck", args)
525
526 @mcp.tool(
527 name="oct_diag",
528 description=(
529 "Run Option C diagnostics. Available subcommands: "
530 "'validate-config' (check .octrc.json), "
531 "'show-config' (display resolved config), "
532 "'list-checks' (list available lint checks)."
533 ),
534 )
535 def oct_diag(
536 json_output: bool = True,
537 subcommand: str = "validate-config",
538 ) -> str:
539 return _run_tool("oct_diag", {"json_output": json_output, "subcommand": subcommand})
540
541 @mcp.tool(
542 name="oct_git_status",
543 description=(
544 "Show the git status of the project: staged files, modified "
545 "files, branch name, and commit counts. Returns a JSON summary."
546 ),
547 )
548 def oct_git_status(
549 json_output: bool = True,
550 verbose: bool = False,
551 ) -> str:
552 return _run_tool("oct_git_status", {"json_output": json_output, "verbose": verbose})
553
554 @mcp.tool(
555 name="oct_git_check",
556 description=(
557 "Run the Option C git pre-commit quality gate check. Verifies "
558 "that staged changes pass lint, format, and secret-detection "
559 "checks. Returns a JSON summary of results."
560 ),
561 )
562 def oct_git_check(
563 json_output: bool = True,
564 verbose: bool = False,
565 ) -> str:
566 return _run_tool("oct_git_check", {"json_output": json_output, "verbose": verbose})
567
568
569def register_write_tools(mcp: FastMCP) -> None:
570 """Register all 11 Phase 5B write tools with the FastMCP server instance.
571
572 All write tools require the user to confirm with ``confirm=True``
573 before execution proceeds (HITL gate). Without confirmation the tool
574 returns a ``[oct-mcp requires_confirmation]`` string describing what
575 would happen.
576
577 Called once during :func:`~oct.mcp.server.start_server`.
578 """
579
580 @mcp.tool(
581 name="oct_format",
582 description=(
583 "Auto-fix Option C compliance violations in Python files. "
584 "REQUIRES CONFIRMATION: call with confirm=True to proceed. "
585 "Set apply=True AND confirm=True to write changes. "
586 "With apply=False (default) runs in --dry-run mode even when confirmed."
587 ),
588 )
589 def oct_format(
590 paths: list[str] | None = None,
591 apply: bool = False,
592 confirm: bool = False,
593 changed: bool = False,
594 verbose: bool = False,
595 jobs: int = 1,
596 json_output: bool = True,
597 ) -> str:
598 return _run_tool_write("oct_format", {
599 "paths": paths or [], "apply": apply, "confirm": confirm,
600 "changed": changed, "verbose": verbose, "jobs": jobs,
601 "json_output": json_output,
602 })
603
604 @mcp.tool(
605 name="oct_docs",
606 description=(
607 "Generate or clean Option C documentation. "
608 "REQUIRES CONFIRMATION: call with confirm=True to proceed. "
609 "Use sphinx=True, doxygen=True, or all_docs=True to select output. "
610 "dry_run=True (default) previews without generating files."
611 ),
612 )
613 def oct_docs(
614 sphinx: bool = False,
615 doxygen: bool = False,
616 all_docs: bool = False,
617 clean: bool = False,
618 dry_run: bool = True,
619 confirm: bool = False,
620 ) -> str:
621 return _run_tool_write("oct_docs", {
622 "sphinx": sphinx, "doxygen": doxygen, "all_docs": all_docs,
623 "clean": clean, "dry_run": dry_run, "confirm": confirm,
624 })
625
626 @mcp.tool(
627 name="oct_export_source",
628 description=(
629 "Export consolidated source-code files for inspection. "
630 "REQUIRES CONFIRMATION: call with confirm=True to proceed. "
631 "Writes _source_code-* export directories to the project root."
632 ),
633 )
634 def oct_export_source(
635 confirm: bool = False,
636 verbose: bool = False,
637 single_dir: bool = False,
638 warn_secrets: bool = True,
639 block_secrets: bool = False,
640 ) -> str:
641 return _run_tool_write("oct_export_source", {
642 "confirm": confirm, "verbose": verbose, "single_dir": single_dir,
643 "warn_secrets": warn_secrets, "block_secrets": block_secrets,
644 })
645
646 @mcp.tool(
647 name="oct_new",
648 description=(
649 "Create a new Option C-compliant Python source file. "
650 "REQUIRES CONFIRMATION: call with confirm=True to proceed. "
651 "dry_run=True (default) shows what would be created without writing."
652 ),
653 )
654 def oct_new(
655 path: str = "",
656 confirm: bool = False,
657 force: bool = False,
658 create_test: bool = False,
659 dry_run: bool = True,
660 verbose: bool = False,
661 ) -> str:
662 return _run_tool_write("oct_new", {
663 "path": path, "confirm": confirm, "force": force,
664 "create_test": create_test, "dry_run": dry_run, "verbose": verbose,
665 })
666
667 @mcp.tool(
668 name="oct_scaffold",
669 description=(
670 "Create a new Option C project directory tree. "
671 "REQUIRES CONFIRMATION: call with confirm=True to proceed. "
672 "dry_run=True (default) shows what would be created without writing."
673 ),
674 )
675 def oct_scaffold(
676 name: str = "",
677 confirm: bool = False,
678 dry_run: bool = True,
679 ) -> str:
680 return _run_tool_write("oct_scaffold", {
681 "name": name, "confirm": confirm, "dry_run": dry_run,
682 })
683
684 @mcp.tool(
685 name="oct_clean",
686 description=(
687 "Remove _source_code-*, _skeleton_code-*, and __pycache__ "
688 "directories from the project. "
689 "REQUIRES CONFIRMATION: call with confirm=True to proceed. "
690 "dry_run=True (default) shows what would be deleted without deleting."
691 ),
692 )
693 def oct_clean(
694 path: str | None = None,
695 confirm: bool = False,
696 dry_run: bool = True,
697 ) -> str:
698 return _run_tool_write("oct_clean", {
699 "path": path, "confirm": confirm, "dry_run": dry_run,
700 })
701
702 @mcp.tool(
703 name="oct_install_hooks",
704 description=(
705 "Install Option C pre-commit hooks configuration. "
706 "REQUIRES CONFIRMATION: call with confirm=True to proceed. "
707 "Writes .pre-commit-config.yaml to the project root."
708 ),
709 )
710 def oct_install_hooks(
711 confirm: bool = False,
712 force: bool = False,
713 ) -> str:
714 return _run_tool_write("oct_install_hooks", {
715 "confirm": confirm, "force": force,
716 })
717
718 @mcp.tool(
719 name="oct_git_commit",
720 description=(
721 "Commit staged changes with Option C quality gate enforcement. "
722 "REQUIRES CONFIRMATION: call with confirm=True to proceed. "
723 "Validates Conventional Commits format and runs lint/format checks. "
724 "Note: --no-verify is not available via MCP."
725 ),
726 )
727 def oct_git_commit(
728 message: str = "",
729 confirm: bool = False,
730 force: bool = False,
731 json_output: bool = True,
732 ) -> str:
733 return _run_tool_write("oct_git_commit", {
734 "message": message, "confirm": confirm, "force": force,
735 "json_output": json_output,
736 })
737
738 @mcp.tool(
739 name="oct_git_hooks_install",
740 description=(
741 "Install Option C git pre-commit hooks via 'oct git hooks install'. "
742 "REQUIRES CONFIRMATION: call with confirm=True to proceed."
743 ),
744 )
745 def oct_git_hooks_install(
746 confirm: bool = False,
747 force: bool = False,
748 ) -> str:
749 return _run_tool_write("oct_git_hooks_install", {
750 "confirm": confirm, "force": force,
751 })
752
753 @mcp.tool(
754 name="oct_git_init",
755 description=(
756 "Initialise or enhance a git repository with Option C conventions "
757 "(gitignore, gitattributes, hooks). "
758 "REQUIRES CONFIRMATION: call with confirm=True to proceed. "
759 "dry_run=True (default) shows what would change without modifying."
760 ),
761 )
762 def oct_git_init(
763 confirm: bool = False,
764 dry_run: bool = True,
765 github: bool = False,
766 no_hooks: bool = False,
767 ) -> str:
768 return _run_tool_write("oct_git_init", {
769 "confirm": confirm, "dry_run": dry_run,
770 "github": github, "no_hooks": no_hooks,
771 })
772
773 @mcp.tool(
774 name="oct_git_changelog",
775 description=(
776 "Generate CHANGELOG.md from Conventional Commits history. "
777 "REQUIRES CONFIRMATION: call with confirm=True to proceed. "
778 "dry_run=True (default) prints to stdout without writing the file."
779 ),
780 )
781 def oct_git_changelog(
782 confirm: bool = False,
783 dry_run: bool = True,
784 since: str | None = None,
785 version: str | None = None,
786 json_output: bool = True,
787 ) -> str:
788 return _run_tool_write("oct_git_changelog", {
789 "confirm": confirm, "dry_run": dry_run,
790 "since": since, "version": version, "json_output": json_output,
791 })
int _elapsed_ms(float start)
Definition tools.py:390
str _run_tool_write(str tool_name, dict raw_args)
Definition tools.py:221
None register_write_tools(FastMCP mcp)
Definition tools.py:569
str _safe_getuser()
Definition tools.py:394
None _dbg(*args, **kwargs)
Definition tools.py:67
str _run_tool(str tool_name, dict raw_args)
Definition tools.py:84
str _error_response(str message, str tool_name)
Definition tools.py:401
None register_tools(FastMCP mcp)
Definition tools.py:411