argus-mcp-cli 0.1.0__py3-none-any.whl
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/__init__.py +0 -0
- argus_mcp/config.py +46 -0
- argus_mcp/server.py +52 -0
- argus_mcp/tools/__init__.py +0 -0
- argus_mcp/tools/context.py +124 -0
- argus_mcp/tools/memory.py +165 -0
- argus_mcp/tools/scaffold.py +251 -0
- argus_mcp/tools/validate.py +237 -0
- argus_mcp_cli-0.1.0.dist-info/METADATA +6 -0
- argus_mcp_cli-0.1.0.dist-info/RECORD +12 -0
- argus_mcp_cli-0.1.0.dist-info/WHEEL +4 -0
- argus_mcp_cli-0.1.0.dist-info/entry_points.txt +2 -0
argus_mcp/__init__.py
ADDED
|
File without changes
|
argus_mcp/config.py
ADDED
|
@@ -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"
|
argus_mcp/server.py
ADDED
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from datetime import date
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import mcp.types as types
|
|
9
|
+
|
|
10
|
+
from argus_mcp.config import load
|
|
11
|
+
|
|
12
|
+
TOOLS: list[types.Tool] = [
|
|
13
|
+
types.Tool(
|
|
14
|
+
name="create_spec",
|
|
15
|
+
description="Cria um novo arquivo de spec no vault a partir do template.",
|
|
16
|
+
input_schema={
|
|
17
|
+
"type": "object",
|
|
18
|
+
"properties": {
|
|
19
|
+
"id": {"type": "string"},
|
|
20
|
+
"title": {"type": "string"},
|
|
21
|
+
},
|
|
22
|
+
"required": ["id", "title"],
|
|
23
|
+
},
|
|
24
|
+
),
|
|
25
|
+
types.Tool(
|
|
26
|
+
name="create_feature",
|
|
27
|
+
description="Cria estrutura VSA de uma nova feature no projeto.",
|
|
28
|
+
input_schema={
|
|
29
|
+
"type": "object",
|
|
30
|
+
"properties": {
|
|
31
|
+
"name": {"type": "string", "description": "Nome em kebab-case"},
|
|
32
|
+
},
|
|
33
|
+
"required": ["name"],
|
|
34
|
+
},
|
|
35
|
+
),
|
|
36
|
+
types.Tool(
|
|
37
|
+
name="create_endpoint",
|
|
38
|
+
description="Cria arquivos de endpoint dentro de uma feature existente.",
|
|
39
|
+
input_schema={
|
|
40
|
+
"type": "object",
|
|
41
|
+
"properties": {
|
|
42
|
+
"feature": {"type": "string"},
|
|
43
|
+
"method": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"enum": ["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
46
|
+
},
|
|
47
|
+
"path": {"type": "string"},
|
|
48
|
+
},
|
|
49
|
+
"required": ["feature", "method", "path"],
|
|
50
|
+
},
|
|
51
|
+
),
|
|
52
|
+
types.Tool(
|
|
53
|
+
name="create_migration",
|
|
54
|
+
description="Cria arquivo de migração numerado sequencialmente.",
|
|
55
|
+
input_schema={
|
|
56
|
+
"type": "object",
|
|
57
|
+
"properties": {
|
|
58
|
+
"description": {"type": "string", "description": "Em snake_case"},
|
|
59
|
+
},
|
|
60
|
+
"required": ["description"],
|
|
61
|
+
},
|
|
62
|
+
),
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def _create_spec(args: dict[str, object]) -> str:
|
|
67
|
+
try:
|
|
68
|
+
cfg = load()
|
|
69
|
+
except RuntimeError as e:
|
|
70
|
+
return f"[ERRO] {e}"
|
|
71
|
+
|
|
72
|
+
spec_id = str(args.get("id", ""))
|
|
73
|
+
title = str(args.get("title", ""))
|
|
74
|
+
slug = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-")
|
|
75
|
+
target = cfg.vault_path / "03-specifications" / f"{spec_id}-{slug}.md"
|
|
76
|
+
|
|
77
|
+
if target.exists():
|
|
78
|
+
return f"[ERRO] spec já existe: {target.relative_to(cfg.vault_path)}"
|
|
79
|
+
|
|
80
|
+
template_file = cfg.vault_path / "99-templates" / "spec.md"
|
|
81
|
+
if template_file.exists():
|
|
82
|
+
content = template_file.read_text(encoding="utf-8")
|
|
83
|
+
else:
|
|
84
|
+
content = _default_spec_template()
|
|
85
|
+
|
|
86
|
+
today = date.today().isoformat()
|
|
87
|
+
content = (
|
|
88
|
+
content.replace("PROJ-SPEC-XXX", spec_id)
|
|
89
|
+
.replace("[título]", title)
|
|
90
|
+
.replace("{{DATE}}", today)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
target.write_text(content, encoding="utf-8")
|
|
95
|
+
return f"Spec criada: vault/03-specifications/{target.name}"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def _create_feature(args: dict[str, object]) -> str:
|
|
99
|
+
try:
|
|
100
|
+
cfg = load()
|
|
101
|
+
except RuntimeError as e:
|
|
102
|
+
return f"[ERRO] {e}"
|
|
103
|
+
|
|
104
|
+
name = str(args.get("name", "")).strip()
|
|
105
|
+
feature_dir = cfg.project_root / "src" / "features" / name
|
|
106
|
+
|
|
107
|
+
if feature_dir.exists():
|
|
108
|
+
return f"[ERRO] feature já existe: src/features/{name}"
|
|
109
|
+
|
|
110
|
+
files = _feature_files(cfg.stack, name)
|
|
111
|
+
for rel_path, content in files.items():
|
|
112
|
+
full = feature_dir / rel_path
|
|
113
|
+
full.parent.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
full.write_text(content, encoding="utf-8")
|
|
115
|
+
|
|
116
|
+
created = "\n".join(f" + {p}" for p in files)
|
|
117
|
+
return f"Feature criada: src/features/{name}/\n{created}"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async def _create_endpoint(args: dict[str, object]) -> str:
|
|
121
|
+
try:
|
|
122
|
+
cfg = load()
|
|
123
|
+
except RuntimeError as e:
|
|
124
|
+
return f"[ERRO] {e}"
|
|
125
|
+
|
|
126
|
+
feature = str(args.get("feature", ""))
|
|
127
|
+
method = str(args.get("method", "GET")).upper()
|
|
128
|
+
path = str(args.get("path", ""))
|
|
129
|
+
feature_dir = cfg.project_root / "src" / "features" / feature
|
|
130
|
+
|
|
131
|
+
if not feature_dir.exists():
|
|
132
|
+
return f"[ERRO] feature não encontrada: src/features/{feature}"
|
|
133
|
+
|
|
134
|
+
note = _endpoint_note(cfg.stack, method, path)
|
|
135
|
+
return f"Endpoint adicionado: {method} {path} em src/features/{feature}/\n{note}"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
async def _create_migration(args: dict[str, object]) -> str:
|
|
139
|
+
try:
|
|
140
|
+
cfg = load()
|
|
141
|
+
except RuntimeError as e:
|
|
142
|
+
return f"[ERRO] {e}"
|
|
143
|
+
|
|
144
|
+
description = str(args.get("description", "")).strip()
|
|
145
|
+
migrations_dir = cfg.project_root / "migrations"
|
|
146
|
+
migrations_dir.mkdir(exist_ok=True)
|
|
147
|
+
|
|
148
|
+
next_num = _next_migration_number(migrations_dir)
|
|
149
|
+
prefix = f"{next_num:04d}_{description}"
|
|
150
|
+
|
|
151
|
+
up_file = migrations_dir / f"{prefix}.up.sql"
|
|
152
|
+
down_file = migrations_dir / f"{prefix}.down.sql"
|
|
153
|
+
|
|
154
|
+
up_file.write_text(f"-- Migration: {description}\n\n", encoding="utf-8")
|
|
155
|
+
down_file.write_text(f"-- Rollback: {description}\n\n", encoding="utf-8")
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
f"Migration criada:\n"
|
|
159
|
+
f" + migrations/{up_file.name}\n"
|
|
160
|
+
f" + migrations/{down_file.name}"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# --- helpers ---
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _next_migration_number(migrations_dir: Path) -> int:
|
|
168
|
+
existing = [
|
|
169
|
+
int(m.name[:4]) for m in migrations_dir.glob("*.sql") if m.name[:4].isdigit()
|
|
170
|
+
]
|
|
171
|
+
return max(existing, default=0) + 1
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _feature_files(stack: str, name: str) -> dict[str, str]:
|
|
175
|
+
snake = name.replace("-", "_")
|
|
176
|
+
if "fastapi" in stack or "python" in stack:
|
|
177
|
+
return {
|
|
178
|
+
"schemas.py": f"from pydantic import BaseModel\n\n\nclass {_pascal(name)}Response(BaseModel):\n pass\n",
|
|
179
|
+
"repository.py": f'from supabase import AsyncClient\n\n\nasync def find_all(db: AsyncClient) -> list[dict]:\n result = await db.table("{snake}s").select("*").execute()\n return result.data\n',
|
|
180
|
+
"service.py": "from supabase import AsyncClient\nfrom . import repository\n\n\nasync def list_all(db: AsyncClient) -> list[dict]:\n return await repository.find_all(db)\n",
|
|
181
|
+
"router.py": f'from fastapi import APIRouter, Depends\nfrom supabase import AsyncClient\nfrom app.deps import get_db\nfrom . import service\n\nrouter = APIRouter(prefix="/{snake}s", tags=["{snake}s"])\n\n\n@router.get("/")\nasync def list_{snake}s(db: AsyncClient = Depends(get_db)):\n return await service.list_all(db)\n',
|
|
182
|
+
f"tests/test_{snake}.py": f'import pytest\nfrom httpx import AsyncClient\n\n\n@pytest.mark.asyncio\nasync def test_list_{snake}s(client: AsyncClient):\n response = await client.get("/{snake}s/")\n assert response.status_code == 200\n',
|
|
183
|
+
}
|
|
184
|
+
if "nextjs" in stack or "next" in stack:
|
|
185
|
+
return {
|
|
186
|
+
"page.tsx": f"export default async function {_pascal(name)}Page() {{\n return <main><h1>{_pascal(name)}</h1></main>;\n}}\n",
|
|
187
|
+
"actions.ts": f'"use server";\n\nexport async function list{_pascal(name)}() {{\n // TODO\n return [];\n}}\n',
|
|
188
|
+
"repository.ts": f'import {{ prisma }} from "@/lib/prisma";\n\nexport async function findAll() {{\n return prisma.{snake}.findMany();\n}}\n',
|
|
189
|
+
"schemas.ts": f'import {{ z }} from "zod";\n\nexport const {snake}Schema = z.object({{\n id: z.string(),\n}});\n',
|
|
190
|
+
f"tests/{snake}.test.ts": f'import {{ describe, it, expect }} from "vitest";\n\ndescribe("{name}", () => {{\n it("placeholder", () => expect(true).toBe(true));\n}});\n',
|
|
191
|
+
}
|
|
192
|
+
if "go" in stack:
|
|
193
|
+
pkg = snake
|
|
194
|
+
return {
|
|
195
|
+
"model.go": f'package {pkg}\n\ntype {_pascal(name)} struct {{\n\tID string `json:"id"`\n}}\n',
|
|
196
|
+
"repository.go": f"package {pkg}\n\ntype Repository struct{{}}\n",
|
|
197
|
+
"service.go": f"package {pkg}\n\ntype Service struct {{\n\trepo *Repository\n}}\n\nfunc NewService(r *Repository) *Service {{ return &Service{{repo: r}} }}\n",
|
|
198
|
+
"handler.go": f'package {pkg}\n\nimport "net/http"\n\ntype Handler struct {{ svc *Service }}\n\nfunc NewHandler(s *Service) *Handler {{ return &Handler{{svc: s}} }}\n\nfunc (h *Handler) List(w http.ResponseWriter, r *http.Request) {{}}\n',
|
|
199
|
+
"handler_test.go": f'package {pkg}_test\n\nimport "testing"\n\nfunc TestList(t *testing.T) {{ t.Skip("implement") }}\n',
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
"README.md": f"# {name}\n\nFeature criada pelo Argus. Stack não reconhecida: {stack}\n"
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _endpoint_note(stack: str, method: str, path: str) -> str:
|
|
207
|
+
if "fastapi" in stack:
|
|
208
|
+
return f' Adicione @router.{method.lower()}("{path}") em router.py'
|
|
209
|
+
if "nextjs" in stack:
|
|
210
|
+
return f" Adicione export async function {method.lower()}() em route.ts"
|
|
211
|
+
if "go" in stack:
|
|
212
|
+
return f' Adicione r.{method}("{path}", h.handler) em handler.go'
|
|
213
|
+
return " Adicione o handler conforme a stack do projeto"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _pascal(name: str) -> str:
|
|
217
|
+
return "".join(w.capitalize() for w in re.split(r"[-_]", name))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _default_spec_template() -> str:
|
|
221
|
+
return """---
|
|
222
|
+
id: {{ID}}
|
|
223
|
+
title: Spec — {{TITLE}}
|
|
224
|
+
type: spec
|
|
225
|
+
status: draft
|
|
226
|
+
created_at: {{DATE}}
|
|
227
|
+
updated_at: {{DATE}}
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
# Spec — {{TITLE}}
|
|
231
|
+
|
|
232
|
+
## Contexto
|
|
233
|
+
|
|
234
|
+
## Comportamento esperado
|
|
235
|
+
|
|
236
|
+
## Critérios de aceitação
|
|
237
|
+
|
|
238
|
+
- [ ] ...
|
|
239
|
+
|
|
240
|
+
## Fora de escopo
|
|
241
|
+
|
|
242
|
+
- ...
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
HANDLERS: dict[str, Any] = {
|
|
247
|
+
"create_spec": _create_spec,
|
|
248
|
+
"create_feature": _create_feature,
|
|
249
|
+
"create_endpoint": _create_endpoint,
|
|
250
|
+
"create_migration": _create_migration,
|
|
251
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import mcp.types as types
|
|
9
|
+
|
|
10
|
+
from argus_mcp.config import load
|
|
11
|
+
|
|
12
|
+
TOOLS: list[types.Tool] = [
|
|
13
|
+
types.Tool(
|
|
14
|
+
name="validate_architecture",
|
|
15
|
+
description="Verifica VSA: sem imports cruzados entre features.",
|
|
16
|
+
input_schema={"type": "object", "properties": {}},
|
|
17
|
+
),
|
|
18
|
+
types.Tool(
|
|
19
|
+
name="check_quality",
|
|
20
|
+
description="Executa lint, typecheck e testes.",
|
|
21
|
+
input_schema={
|
|
22
|
+
"type": "object",
|
|
23
|
+
"properties": {
|
|
24
|
+
"fix": {"type": "boolean", "description": "Corrige automaticamente"},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
),
|
|
28
|
+
types.Tool(
|
|
29
|
+
name="review_endpoint",
|
|
30
|
+
description="Verifica checklist de endpoint completo.",
|
|
31
|
+
input_schema={
|
|
32
|
+
"type": "object",
|
|
33
|
+
"properties": {
|
|
34
|
+
"feature": {"type": "string"},
|
|
35
|
+
"path": {"type": "string"},
|
|
36
|
+
},
|
|
37
|
+
"required": ["feature", "path"],
|
|
38
|
+
},
|
|
39
|
+
),
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def _validate_architecture(args: dict[str, object]) -> str:
|
|
44
|
+
try:
|
|
45
|
+
cfg = load()
|
|
46
|
+
except RuntimeError as e:
|
|
47
|
+
return f"[ERRO] {e}"
|
|
48
|
+
|
|
49
|
+
features_dir = cfg.project_root / "src" / "features"
|
|
50
|
+
if not features_dir.exists():
|
|
51
|
+
return "[ERRO] src/features/ não encontrado — rode create_feature primeiro"
|
|
52
|
+
|
|
53
|
+
features = [d.name for d in features_dir.iterdir() if d.is_dir()]
|
|
54
|
+
violations = _find_cross_imports(features_dir, features)
|
|
55
|
+
|
|
56
|
+
if not violations:
|
|
57
|
+
return (
|
|
58
|
+
f"Arquitetura válida.\n"
|
|
59
|
+
f"Analisadas: {len(features)} features\n"
|
|
60
|
+
f"Imports cruzados: nenhum"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
lines = ["[AVISO] Imports cruzados detectados:"]
|
|
64
|
+
lines.extend(f" {src} → {dst}" for src, dst in violations)
|
|
65
|
+
return "\n".join(lines)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def _check_quality(args: dict[str, object]) -> str:
|
|
69
|
+
try:
|
|
70
|
+
cfg = load()
|
|
71
|
+
except RuntimeError as e:
|
|
72
|
+
return f"[ERRO] {e}"
|
|
73
|
+
|
|
74
|
+
fix = bool(args.get("fix", False))
|
|
75
|
+
stack = cfg.stack
|
|
76
|
+
commands = _quality_commands(stack, fix, cfg.project_root)
|
|
77
|
+
|
|
78
|
+
results: list[tuple[str, bool, str]] = []
|
|
79
|
+
for label, cmd in commands:
|
|
80
|
+
ok, output = await _run(cmd, cwd=cfg.project_root)
|
|
81
|
+
results.append((label, ok, output))
|
|
82
|
+
|
|
83
|
+
all_passed = all(ok for _, ok, _ in results)
|
|
84
|
+
header = "Qualidade OK" if all_passed else "[ERRO] Qualidade falhou"
|
|
85
|
+
lines = [header]
|
|
86
|
+
for label, ok, output in results:
|
|
87
|
+
status = "PASSOU" if ok else "FALHOU"
|
|
88
|
+
lines.append(f" {label:<12} {status}")
|
|
89
|
+
if not ok:
|
|
90
|
+
lines.extend(f" {line}" for line in output.strip().splitlines()[:10])
|
|
91
|
+
|
|
92
|
+
return "\n".join(lines)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def _review_endpoint(args: dict[str, object]) -> str:
|
|
96
|
+
try:
|
|
97
|
+
cfg = load()
|
|
98
|
+
except RuntimeError as e:
|
|
99
|
+
return f"[ERRO] {e}"
|
|
100
|
+
|
|
101
|
+
feature = str(args.get("feature", ""))
|
|
102
|
+
path = str(args.get("path", ""))
|
|
103
|
+
feature_dir = cfg.project_root / "src" / "features" / feature
|
|
104
|
+
|
|
105
|
+
if not feature_dir.exists():
|
|
106
|
+
return f"[ERRO] feature não encontrada: src/features/{feature}"
|
|
107
|
+
|
|
108
|
+
checklist = _build_checklist(feature_dir, cfg.stack)
|
|
109
|
+
all_passed = all(ok for _, ok, _ in checklist)
|
|
110
|
+
|
|
111
|
+
status = "aprovado" if all_passed else "incompleto"
|
|
112
|
+
header = f"Endpoint {status}: {path} em {feature}"
|
|
113
|
+
lines = [header]
|
|
114
|
+
for item, ok, note in checklist:
|
|
115
|
+
mark = "✓" if ok else "✗"
|
|
116
|
+
line = f" {mark} {item}"
|
|
117
|
+
if note:
|
|
118
|
+
line += f" — {note}"
|
|
119
|
+
lines.append(line)
|
|
120
|
+
|
|
121
|
+
return "\n".join(lines)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# --- helpers ---
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _find_cross_imports(
|
|
128
|
+
features_dir: Path, features: list[str]
|
|
129
|
+
) -> list[tuple[str, str]]:
|
|
130
|
+
violations: list[tuple[str, str]] = []
|
|
131
|
+
for feature in features:
|
|
132
|
+
feature_path = features_dir / feature
|
|
133
|
+
for src_file in feature_path.rglob("*"):
|
|
134
|
+
if src_file.suffix not in (".py", ".ts", ".tsx", ".go"):
|
|
135
|
+
continue
|
|
136
|
+
try:
|
|
137
|
+
content = src_file.read_text(encoding="utf-8", errors="ignore")
|
|
138
|
+
except OSError:
|
|
139
|
+
continue
|
|
140
|
+
for other in features:
|
|
141
|
+
if other == feature:
|
|
142
|
+
continue
|
|
143
|
+
if _imports_feature(content, other, src_file.suffix):
|
|
144
|
+
rel = src_file.relative_to(features_dir.parent.parent)
|
|
145
|
+
violations.append((str(rel), f"features/{other}"))
|
|
146
|
+
return violations
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _imports_feature(content: str, other_feature: str, suffix: str) -> bool:
|
|
150
|
+
snake = other_feature.replace("-", "_")
|
|
151
|
+
if suffix == ".py":
|
|
152
|
+
return bool(
|
|
153
|
+
re.search(rf"from features\.{snake}\.|import features\.{snake}", content)
|
|
154
|
+
)
|
|
155
|
+
if suffix in (".ts", ".tsx"):
|
|
156
|
+
return bool(re.search(rf"from ['\"].*features/{other_feature}", content))
|
|
157
|
+
if suffix == ".go":
|
|
158
|
+
return bool(re.search(rf"\".*features/{other_feature}\"", content))
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _quality_commands(stack: str, fix: bool, root: Path) -> list[tuple[str, list[str]]]:
|
|
163
|
+
if "fastapi" in stack or "python" in stack:
|
|
164
|
+
lint_cmd = ["ruff", "check", ".", "--fix"] if fix else ["ruff", "check", "."]
|
|
165
|
+
return [
|
|
166
|
+
("lint", lint_cmd),
|
|
167
|
+
("typecheck", ["mypy", "src/"]),
|
|
168
|
+
("testes", ["pytest", "--tb=short", "-q"]),
|
|
169
|
+
]
|
|
170
|
+
if "nextjs" in stack or "next" in stack:
|
|
171
|
+
lint_cmd = ["pnpm", "lint", "--fix"] if fix else ["pnpm", "lint"]
|
|
172
|
+
return [
|
|
173
|
+
("lint", lint_cmd),
|
|
174
|
+
("typecheck", ["pnpm", "type-check"]),
|
|
175
|
+
("testes", ["pnpm", "test", "--run"]),
|
|
176
|
+
]
|
|
177
|
+
if "go" in stack:
|
|
178
|
+
return [
|
|
179
|
+
("vet", ["go", "vet", "./..."]),
|
|
180
|
+
("lint", ["golangci-lint", "run", "./..."]),
|
|
181
|
+
("testes", ["go", "test", "./..."]),
|
|
182
|
+
]
|
|
183
|
+
return [("testes", ["echo", "stack não reconhecida"])]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
async def _run(cmd: list[str], cwd: Path) -> tuple[bool, str]:
|
|
187
|
+
try:
|
|
188
|
+
proc = await asyncio.create_subprocess_exec(
|
|
189
|
+
*cmd,
|
|
190
|
+
cwd=cwd,
|
|
191
|
+
stdout=asyncio.subprocess.PIPE,
|
|
192
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
193
|
+
)
|
|
194
|
+
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=60)
|
|
195
|
+
output = stdout.decode(errors="replace")
|
|
196
|
+
return proc.returncode == 0, output
|
|
197
|
+
except FileNotFoundError:
|
|
198
|
+
return False, f"[comando não encontrado: {cmd[0]}]"
|
|
199
|
+
except TimeoutError:
|
|
200
|
+
return False, "[timeout após 60s]"
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _build_checklist(feature_dir: Path, stack: str) -> list[tuple[str, bool, str]]:
|
|
204
|
+
files = {f.name for f in feature_dir.rglob("*") if f.is_file()}
|
|
205
|
+
has_test = any("test" in f.lower() for f in files)
|
|
206
|
+
|
|
207
|
+
if "fastapi" in stack or "python" in stack:
|
|
208
|
+
return [
|
|
209
|
+
("schema Pydantic definido", "schemas.py" in files, ""),
|
|
210
|
+
("rota em router.py", "router.py" in files, ""),
|
|
211
|
+
("lógica no service.py", "service.py" in files, ""),
|
|
212
|
+
("acesso ao banco via repository.py", "repository.py" in files, ""),
|
|
213
|
+
("teste presente", has_test, "crie em tests/"),
|
|
214
|
+
]
|
|
215
|
+
if "nextjs" in stack or "next" in stack:
|
|
216
|
+
return [
|
|
217
|
+
("schema Zod definido", "schemas.ts" in files, ""),
|
|
218
|
+
("server action em actions.ts", "actions.ts" in files, ""),
|
|
219
|
+
("acesso ao banco via repository.ts", "repository.ts" in files, ""),
|
|
220
|
+
("page.tsx presente", "page.tsx" in files, ""),
|
|
221
|
+
("teste presente", has_test, "crie em tests/"),
|
|
222
|
+
]
|
|
223
|
+
if "go" in stack:
|
|
224
|
+
return [
|
|
225
|
+
("handler.go presente", "handler.go" in files, ""),
|
|
226
|
+
("service.go presente", "service.go" in files, ""),
|
|
227
|
+
("repository.go presente", "repository.go" in files, ""),
|
|
228
|
+
("teste presente", has_test, "crie handler_test.go"),
|
|
229
|
+
]
|
|
230
|
+
return [("estrutura de feature", feature_dir.exists(), "")]
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
HANDLERS: dict[str, Any] = {
|
|
234
|
+
"validate_architecture": _validate_architecture,
|
|
235
|
+
"check_quality": _check_quality,
|
|
236
|
+
"review_endpoint": _review_endpoint,
|
|
237
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
argus_mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
argus_mcp/config.py,sha256=SC2jiaCBpTlB_R6tZxpelMDAE6geaZGE3BETUCroy7g,1185
|
|
3
|
+
argus_mcp/server.py,sha256=UQUwVV5kYndbUDQ1_e_cCesF4gslVsk6ttkw_Wy1Aew,1345
|
|
4
|
+
argus_mcp/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
argus_mcp/tools/context.py,sha256=I1pIZy2sPrRZSYtEdoIaebZ-lSn7r7CTQ-5pqKfxl04,3763
|
|
6
|
+
argus_mcp/tools/memory.py,sha256=V6INVLZlpAggHyMFDrYD4jmRWILtA2oAmBYYlm8Rjtk,5021
|
|
7
|
+
argus_mcp/tools/scaffold.py,sha256=6BYeD4yRkqKX6J0SN1vj9xOZgwE_bY2dh7MT0XkW3_w,9234
|
|
8
|
+
argus_mcp/tools/validate.py,sha256=5m6GaI_rSVnsb3UvlZxec1-Pp_K_Vn1OTzerbPLDsY8,8046
|
|
9
|
+
argus_mcp_cli-0.1.0.dist-info/METADATA,sha256=-KXfYzdmoCZVWkjSk2Wdix0whja06D50MZunWWQtGQQ,180
|
|
10
|
+
argus_mcp_cli-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
11
|
+
argus_mcp_cli-0.1.0.dist-info/entry_points.txt,sha256=v0Rrcmt2pnSqBxzzYFdt-5iPQw5eAcE054JbxsHnkIs,52
|
|
12
|
+
argus_mcp_cli-0.1.0.dist-info/RECORD,,
|