argus-mcp-cli 0.1.0__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.
- argus_mcp_cli-0.1.0/.gitignore +9 -0
- argus_mcp_cli-0.1.0/PKG-INFO +6 -0
- argus_mcp_cli-0.1.0/pyproject.toml +41 -0
- argus_mcp_cli-0.1.0/src/argus_mcp/__init__.py +0 -0
- argus_mcp_cli-0.1.0/src/argus_mcp/config.py +46 -0
- argus_mcp_cli-0.1.0/src/argus_mcp/server.py +52 -0
- argus_mcp_cli-0.1.0/src/argus_mcp/tools/__init__.py +0 -0
- argus_mcp_cli-0.1.0/src/argus_mcp/tools/context.py +124 -0
- argus_mcp_cli-0.1.0/src/argus_mcp/tools/memory.py +165 -0
- argus_mcp_cli-0.1.0/src/argus_mcp/tools/scaffold.py +251 -0
- argus_mcp_cli-0.1.0/src/argus_mcp/tools/validate.py +237 -0
- argus_mcp_cli-0.1.0/tests/conftest.py +0 -0
- argus_mcp_cli-0.1.0/tests/tools/test_context.py +66 -0
- argus_mcp_cli-0.1.0/tests/tools/test_memory.py +109 -0
- argus_mcp_cli-0.1.0/tests/tools/test_scaffold.py +118 -0
- argus_mcp_cli-0.1.0/tests/tools/test_validate.py +121 -0
- argus_mcp_cli-0.1.0/uv.lock +1000 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "argus-mcp-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Argus MCP server — ferramentas de engenharia para agentes de IA"
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"mcp>=1.0",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
argus-mcp = "argus_mcp.server:main"
|
|
16
|
+
|
|
17
|
+
[tool.hatch.build.targets.wheel]
|
|
18
|
+
packages = ["src/argus_mcp"]
|
|
19
|
+
|
|
20
|
+
[dependency-groups]
|
|
21
|
+
dev = [
|
|
22
|
+
"pytest>=8.0",
|
|
23
|
+
"pytest-asyncio>=0.24",
|
|
24
|
+
"ruff>=0.5",
|
|
25
|
+
"mypy>=1.10",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[tool.pytest.ini_options]
|
|
29
|
+
asyncio_mode = "auto"
|
|
30
|
+
testpaths = ["tests"]
|
|
31
|
+
|
|
32
|
+
[tool.ruff]
|
|
33
|
+
line-length = 88
|
|
34
|
+
target-version = "py312"
|
|
35
|
+
|
|
36
|
+
[tool.ruff.lint]
|
|
37
|
+
select = ["E", "F", "I", "UP"]
|
|
38
|
+
ignore = ["E501"]
|
|
39
|
+
|
|
40
|
+
[tool.mypy]
|
|
41
|
+
strict = true
|
|
File without changes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class Config:
|
|
11
|
+
project_root: Path
|
|
12
|
+
vault_path: Path
|
|
13
|
+
stack: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load() -> Config:
|
|
17
|
+
if root := os.getenv("ARGUS_PROJECT_ROOT"):
|
|
18
|
+
project_root = Path(root)
|
|
19
|
+
else:
|
|
20
|
+
project_root = _find_project_root()
|
|
21
|
+
|
|
22
|
+
if vault := os.getenv("ARGUS_VAULT_PATH"):
|
|
23
|
+
vault_path = Path(vault)
|
|
24
|
+
else:
|
|
25
|
+
vault_path = project_root / "vault"
|
|
26
|
+
|
|
27
|
+
return Config(
|
|
28
|
+
project_root=project_root,
|
|
29
|
+
vault_path=vault_path,
|
|
30
|
+
stack=_read_stack(project_root),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _find_project_root() -> Path:
|
|
35
|
+
for directory in [Path.cwd(), *Path.cwd().parents]:
|
|
36
|
+
if (directory / ".argus").exists() or (directory / "CLAUDE.md").exists():
|
|
37
|
+
return directory
|
|
38
|
+
raise RuntimeError("project root não encontrado — rode argus init primeiro")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _read_stack(root: Path) -> str:
|
|
42
|
+
config_file = root / ".argus" / "config.json"
|
|
43
|
+
if config_file.exists():
|
|
44
|
+
data = json.loads(config_file.read_text(encoding="utf-8"))
|
|
45
|
+
return str(data.get("stack", "unknown"))
|
|
46
|
+
return "unknown"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Callable, Coroutine
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import mcp.types as types
|
|
8
|
+
from mcp.server import Server
|
|
9
|
+
from mcp.server.stdio import stdio_server
|
|
10
|
+
|
|
11
|
+
from argus_mcp.tools import context, memory, scaffold, validate
|
|
12
|
+
|
|
13
|
+
server = Server("argus")
|
|
14
|
+
|
|
15
|
+
_tools: list[types.Tool] = [
|
|
16
|
+
*context.TOOLS,
|
|
17
|
+
*memory.TOOLS,
|
|
18
|
+
*scaffold.TOOLS,
|
|
19
|
+
*validate.TOOLS,
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
Handler = Callable[[dict[str, Any]], Coroutine[Any, Any, str]]
|
|
23
|
+
|
|
24
|
+
_handlers: dict[str, Handler] = {
|
|
25
|
+
**context.HANDLERS,
|
|
26
|
+
**memory.HANDLERS,
|
|
27
|
+
**scaffold.HANDLERS,
|
|
28
|
+
**validate.HANDLERS,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@server.list_tools() # type: ignore[attr-defined, untyped-decorator]
|
|
33
|
+
async def list_tools() -> list[types.Tool]:
|
|
34
|
+
return _tools
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@server.call_tool() # type: ignore[attr-defined, untyped-decorator]
|
|
38
|
+
async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.TextContent]:
|
|
39
|
+
handler = _handlers.get(name)
|
|
40
|
+
if handler is None:
|
|
41
|
+
text = f"[ERRO] tool desconhecida: {name}"
|
|
42
|
+
else:
|
|
43
|
+
text = await handler(arguments or {})
|
|
44
|
+
return [types.TextContent(type="text", text=text)]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def main() -> None:
|
|
48
|
+
async def _run() -> None:
|
|
49
|
+
async with stdio_server() as (read, write):
|
|
50
|
+
await server.run(read, write, server.create_initialization_options())
|
|
51
|
+
|
|
52
|
+
asyncio.run(_run())
|
|
File without changes
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import mcp.types as types
|
|
8
|
+
|
|
9
|
+
from argus_mcp.config import load
|
|
10
|
+
|
|
11
|
+
TOOLS: list[types.Tool] = [
|
|
12
|
+
types.Tool(
|
|
13
|
+
name="read_project_context",
|
|
14
|
+
description="Retorna contexto consolidado do projeto (stack, backlog em andamento, últimas decisões, instruções do agente).",
|
|
15
|
+
input_schema={"type": "object", "properties": {}},
|
|
16
|
+
),
|
|
17
|
+
types.Tool(
|
|
18
|
+
name="read_spec",
|
|
19
|
+
description="Lê um arquivo de spec do vault por ID ou path.",
|
|
20
|
+
input_schema={
|
|
21
|
+
"type": "object",
|
|
22
|
+
"properties": {
|
|
23
|
+
"id": {"type": "string", "description": "ID do item (ex: ARGUS-015)"},
|
|
24
|
+
"path": {"type": "string", "description": "Path relativo ao vault"},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def _read_project_context(args: dict[str, object]) -> str:
|
|
32
|
+
try:
|
|
33
|
+
cfg = load()
|
|
34
|
+
except RuntimeError as e:
|
|
35
|
+
return f"[ERRO] {e}"
|
|
36
|
+
|
|
37
|
+
parts: list[str] = ["# Contexto do projeto"]
|
|
38
|
+
|
|
39
|
+
parts.append(f"\n## Stack\n{cfg.stack}")
|
|
40
|
+
|
|
41
|
+
backlog_file = cfg.vault_path / "03-specifications" / "backlog.md"
|
|
42
|
+
if backlog_file.exists():
|
|
43
|
+
in_progress = _extract_in_progress(backlog_file.read_text(encoding="utf-8"))
|
|
44
|
+
parts.append(f"\n## Backlog (em andamento)\n{in_progress}")
|
|
45
|
+
|
|
46
|
+
decision_file = cfg.vault_path / "00-project-charter" / "decision-log.md"
|
|
47
|
+
if decision_file.exists():
|
|
48
|
+
decisions = _extract_last_decisions(
|
|
49
|
+
decision_file.read_text(encoding="utf-8"), n=5
|
|
50
|
+
)
|
|
51
|
+
parts.append(f"\n## Últimas decisões\n{decisions}")
|
|
52
|
+
|
|
53
|
+
instrucao_file = cfg.vault_path / "11-ai-context" / "instrucao-ia.md"
|
|
54
|
+
if instrucao_file.exists():
|
|
55
|
+
parts.append(
|
|
56
|
+
f"\n## Instruções do agente\n{instrucao_file.read_text(encoding='utf-8')}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return "\n".join(parts)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def _read_spec(args: dict[str, object]) -> str:
|
|
63
|
+
spec_id = args.get("id")
|
|
64
|
+
spec_path = args.get("path")
|
|
65
|
+
|
|
66
|
+
if not spec_id and not spec_path:
|
|
67
|
+
return "[ERRO] forneça id ou path"
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
cfg = load()
|
|
71
|
+
except RuntimeError as e:
|
|
72
|
+
return f"[ERRO] {e}"
|
|
73
|
+
|
|
74
|
+
target: Path | None
|
|
75
|
+
if spec_path:
|
|
76
|
+
target = cfg.vault_path / str(spec_path)
|
|
77
|
+
else:
|
|
78
|
+
target = _find_by_id(cfg.vault_path, str(spec_id))
|
|
79
|
+
|
|
80
|
+
if target is None or not target.exists():
|
|
81
|
+
return f"[ERRO] spec não encontrada: {spec_id or spec_path}"
|
|
82
|
+
|
|
83
|
+
return target.read_text(encoding="utf-8")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _extract_in_progress(backlog: str) -> str:
|
|
87
|
+
match = re.search(r"## Em andamento\n(.*?)(?=\n---|\Z)", backlog, re.DOTALL)
|
|
88
|
+
if not match:
|
|
89
|
+
return "nenhum item em andamento"
|
|
90
|
+
block = match.group(1).strip()
|
|
91
|
+
rows = [
|
|
92
|
+
row
|
|
93
|
+
for row in block.splitlines()
|
|
94
|
+
if row.startswith("|") and "---" not in row and "ID" not in row
|
|
95
|
+
]
|
|
96
|
+
return "\n".join(rows) if rows else "nenhum item em andamento"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _extract_last_decisions(log: str, n: int) -> str:
|
|
100
|
+
entries = re.split(r"\n(?=## \[)", log)
|
|
101
|
+
decision_entries = [e.strip() for e in entries if e.strip().startswith("## [")]
|
|
102
|
+
last = decision_entries[-n:] if len(decision_entries) >= n else decision_entries
|
|
103
|
+
return "\n\n".join(last)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _find_by_id(vault: Path, spec_id: str) -> Path | None:
|
|
107
|
+
for md in vault.rglob("*.md"):
|
|
108
|
+
if spec_id.lower() in md.stem.lower():
|
|
109
|
+
return md
|
|
110
|
+
try:
|
|
111
|
+
if (
|
|
112
|
+
spec_id.lower()
|
|
113
|
+
in md.read_text(encoding="utf-8", errors="ignore").lower()[:200]
|
|
114
|
+
):
|
|
115
|
+
return md
|
|
116
|
+
except OSError:
|
|
117
|
+
continue
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
HANDLERS: dict[str, Any] = {
|
|
122
|
+
"read_project_context": _read_project_context,
|
|
123
|
+
"read_spec": _read_spec,
|
|
124
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from datetime import date
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import mcp.types as types
|
|
8
|
+
|
|
9
|
+
from argus_mcp.config import load
|
|
10
|
+
|
|
11
|
+
TOOLS: list[types.Tool] = [
|
|
12
|
+
types.Tool(
|
|
13
|
+
name="save_decision",
|
|
14
|
+
description="Acrescenta uma entrada formatada no decision-log.md do vault.",
|
|
15
|
+
input_schema={
|
|
16
|
+
"type": "object",
|
|
17
|
+
"properties": {
|
|
18
|
+
"title": {"type": "string"},
|
|
19
|
+
"context": {"type": "string"},
|
|
20
|
+
"decision": {"type": "string"},
|
|
21
|
+
"reason": {"type": "string"},
|
|
22
|
+
"discarded": {"type": "string"},
|
|
23
|
+
},
|
|
24
|
+
"required": ["title", "context", "decision", "reason"],
|
|
25
|
+
},
|
|
26
|
+
),
|
|
27
|
+
types.Tool(
|
|
28
|
+
name="update_backlog",
|
|
29
|
+
description="Move um item do backlog (start | complete | block).",
|
|
30
|
+
input_schema={
|
|
31
|
+
"type": "object",
|
|
32
|
+
"properties": {
|
|
33
|
+
"id": {"type": "string"},
|
|
34
|
+
"action": {"type": "string", "enum": ["start", "complete", "block"]},
|
|
35
|
+
"note": {"type": "string"},
|
|
36
|
+
},
|
|
37
|
+
"required": ["id", "action"],
|
|
38
|
+
},
|
|
39
|
+
),
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def _save_decision(args: dict[str, object]) -> str:
|
|
44
|
+
try:
|
|
45
|
+
cfg = load()
|
|
46
|
+
except RuntimeError as e:
|
|
47
|
+
return f"[ERRO] {e}"
|
|
48
|
+
|
|
49
|
+
decision_file = cfg.vault_path / "00-project-charter" / "decision-log.md"
|
|
50
|
+
if not decision_file.exists():
|
|
51
|
+
return f"[ERRO] decision-log.md não encontrado em {decision_file}"
|
|
52
|
+
|
|
53
|
+
today = date.today().isoformat()
|
|
54
|
+
title = args.get("title", "")
|
|
55
|
+
entry_lines = [
|
|
56
|
+
f"\n## [{today}] {title}",
|
|
57
|
+
f"\n**Contexto:** {args.get('context', '')}",
|
|
58
|
+
f"**Decisão:** {args.get('decision', '')}",
|
|
59
|
+
f"**Motivo:** {args.get('reason', '')}",
|
|
60
|
+
]
|
|
61
|
+
if discarded := args.get("discarded"):
|
|
62
|
+
entry_lines.append(f"**Alternativas descartadas:** {discarded}")
|
|
63
|
+
|
|
64
|
+
entry = "\n".join(entry_lines)
|
|
65
|
+
current = decision_file.read_text(encoding="utf-8")
|
|
66
|
+
decision_file.write_text(current.rstrip() + "\n" + entry + "\n", encoding="utf-8")
|
|
67
|
+
|
|
68
|
+
return f"Decisão salva: {title}"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def _update_backlog(args: dict[str, object]) -> str:
|
|
72
|
+
try:
|
|
73
|
+
cfg = load()
|
|
74
|
+
except RuntimeError as e:
|
|
75
|
+
return f"[ERRO] {e}"
|
|
76
|
+
|
|
77
|
+
backlog_file = cfg.vault_path / "03-specifications" / "backlog.md"
|
|
78
|
+
if not backlog_file.exists():
|
|
79
|
+
return f"[ERRO] backlog.md não encontrado em {backlog_file}"
|
|
80
|
+
|
|
81
|
+
item_id = str(args.get("id", ""))
|
|
82
|
+
action = str(args.get("action", ""))
|
|
83
|
+
note = str(args.get("note", ""))
|
|
84
|
+
|
|
85
|
+
if action not in ("start", "complete", "block"):
|
|
86
|
+
return f"[ERRO] action inválida: {action}"
|
|
87
|
+
|
|
88
|
+
content = backlog_file.read_text(encoding="utf-8")
|
|
89
|
+
|
|
90
|
+
if action == "start":
|
|
91
|
+
content = _move_to_in_progress(content, item_id)
|
|
92
|
+
elif action == "complete":
|
|
93
|
+
content = _move_to_complete(content, item_id)
|
|
94
|
+
elif action == "block":
|
|
95
|
+
content = _add_block_note(content, item_id, note)
|
|
96
|
+
|
|
97
|
+
if content is None:
|
|
98
|
+
return f"[ERRO] item não encontrado: {item_id}"
|
|
99
|
+
|
|
100
|
+
backlog_file.write_text(content, encoding="utf-8")
|
|
101
|
+
return f"Backlog atualizado: {item_id} → {action}"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _find_item_row(content: str, item_id: str) -> re.Match[str] | None:
|
|
105
|
+
pattern = rf"(\| {re.escape(item_id)} \|[^\n]+)"
|
|
106
|
+
return re.search(pattern, content)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _move_to_in_progress(content: str, item_id: str) -> str:
|
|
110
|
+
match = _find_item_row(content, item_id)
|
|
111
|
+
if not match:
|
|
112
|
+
return content
|
|
113
|
+
|
|
114
|
+
row = match.group(1)
|
|
115
|
+
content = content.replace(row, "", 1)
|
|
116
|
+
|
|
117
|
+
in_progress_header = (
|
|
118
|
+
"## Em andamento\n\n| ID | Item | Responsável |\n|----|------|-------------|"
|
|
119
|
+
)
|
|
120
|
+
insert_row = f"| {item_id} | {_extract_item_text(row)} | — |"
|
|
121
|
+
return content.replace(
|
|
122
|
+
in_progress_header,
|
|
123
|
+
f"{in_progress_header}\n{insert_row}",
|
|
124
|
+
1,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _move_to_complete(content: str, item_id: str) -> str:
|
|
129
|
+
match = _find_item_row(content, item_id)
|
|
130
|
+
if not match:
|
|
131
|
+
return content
|
|
132
|
+
|
|
133
|
+
row = match.group(1)
|
|
134
|
+
item_text = _extract_item_text(row)
|
|
135
|
+
today = date.today().isoformat()
|
|
136
|
+
content = content.replace(row, "", 1)
|
|
137
|
+
|
|
138
|
+
complete_row = f"| ~~{item_id}~~ | ~~{item_text}~~ | {today} |"
|
|
139
|
+
concluido_marker = "## Concluído\n\n| ID | Item | Data |\n|----|------|------|"
|
|
140
|
+
return content.replace(
|
|
141
|
+
concluido_marker,
|
|
142
|
+
f"{concluido_marker}\n{complete_row}",
|
|
143
|
+
1,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _add_block_note(content: str, item_id: str, note: str) -> str:
|
|
148
|
+
match = _find_item_row(content, item_id)
|
|
149
|
+
if not match:
|
|
150
|
+
return content
|
|
151
|
+
|
|
152
|
+
row = match.group(1)
|
|
153
|
+
suffix = f" ⚠️ {note}" if note else " ⚠️ bloqueado"
|
|
154
|
+
return content.replace(row, row.rstrip() + suffix, 1)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _extract_item_text(row: str) -> str:
|
|
158
|
+
parts = [p.strip() for p in row.split("|") if p.strip()]
|
|
159
|
+
return parts[1] if len(parts) > 1 else row
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
HANDLERS: dict[str, Any] = {
|
|
163
|
+
"save_decision": _save_decision,
|
|
164
|
+
"update_backlog": _update_backlog,
|
|
165
|
+
}
|