Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONPATH=/app \
AAIS_RUNTIME_DIR=/app/.runtime/aais-data \
AAIS_WORKSPACE_ROOT=/app \
PATH=/usr/local/bin:$PATH \
PORT=8000

Expand Down
1 change: 1 addition & 0 deletions deploy/pilot/Dockerfile.aais
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONPATH=/app \
AAIS_RUNTIME_DIR=/app/.runtime/aais-data \
AAIS_WORKSPACE_ROOT=/app \
PIP_NO_CACHE_DIR=1

WORKDIR /app
Expand Down
1 change: 1 addition & 0 deletions deploy/platform/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONPATH=/app \
AAIS_WORKSPACE_ROOT=/app \
PIP_NO_CACHE_DIR=1

WORKDIR /app
Expand Down
30 changes: 29 additions & 1 deletion governance/mcp_server_manifest.v1.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
{
"mcp_server_manifest_version": "mcp_server_manifest.v1",
"generated_by": "manual",
"servers": {}
"servers": {
"aais-tools-mcp": {
"server_id": "aais-tools-mcp",
"display_name": "AAIS Operator Tools MCP",
"transport": "stdio",
"command": "python",
"args": ["-m", "aais_tools_mcp"],
"cwd": "services/aais-tools-mcp",
"env_keys": [
"AAIS_WORKSPACE_ROOT",
"AAIS_TOOLS_MCP_ALLOW_WRITES",
"AAIS_JARVIS_TOOLS_MCP",
"AAIS_TOOLS_MCP_CMD",
"AAIS_TOOLS_MCP_TIMEOUT_SEC"
],
"tools": [
"read_file",
"write_file",
"apply_patch",
"list_dir",
"search_code",
"run_tests",
"git_status",
"git_diff"
],
"writes_default": false,
"notes": "Governed workspace R/W + allowlisted tests; Jarvis prefers stdio when AAIS_JARVIS_TOOLS_MCP=1 (fail-open to local adapter). See services/aais-tools-mcp/README.md"
}
}
}
2 changes: 2 additions & 0 deletions render.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ services:
value: "8000"
- key: AAIS_RUNTIME_DIR
value: /app/.runtime/aais-data
- key: AAIS_WORKSPACE_ROOT
value: /app
- key: JARVIS_DATA_DIR
value: /app/.runtime/aais-data
- key: ENVIRONMENT
Expand Down
110 changes: 110 additions & 0 deletions services/aais-tools-mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# AAIS Tools MCP

**Engineering:** `AaisOperatorToolServer` / `AaisOperatorToolCatalog`
**Mythic (docs only):** Operator Workshop Tools

Governed Model Context Protocol (stdio JSON-RPC) server so Cursor and Jarvis/AAIS can **read, write, search, and run allowlisted tests** inside the Project Infinity workspace — with path sandboxing, write policy gates, and mutation evidence logs.

Matches the dependency-light pattern used by `services/jarvis-memoryboard/mcp` (stdlib only; no `@modelcontextprotocol` SDK required).

## Tools

| Tool | Mode | Notes |
|------|------|--------|
| `read_file` | read | Sandboxed text read |
| `write_file` | write | Needs `AAIS_TOOLS_MCP_ALLOW_WRITES=1` **and** `allow_write=true`; audited |
| `apply_patch` | write | Full replace or unique `old_string`/`new_string`; same write policy |
| `list_dir` | read | Directory listing |
| `search_code` | read | Bounded regex search |
| `run_tests` | allowlisted | `pytest` or `npm_test` only — **not** arbitrary shell |
| `git_status` | read | `git status --short --branch` |
| `git_diff` | read | `git diff` / `--cached` |

### Hard constraints

- Paths must be **relative** to the workspace root (`AAIS_WORKSPACE_ROOT`, else repo root).
- Refuses `..`, absolute escapes, `.env*`, credentials/secrets, `.runtime/oauth`, `.ssh`, etc.
- Writes are off by default; every successful mutation appends JSONL under `.runtime/aais-tools-mcp/mutations.jsonl` (gitignored via `.runtime/`).
- No offensive/unrestricted shell.

## Cursor MCP config

Add to project `.cursor/mcp.json` or user `~/.cursor/mcp.json`:

```json
{
"mcpServers": {
"aais-tools": {
"command": "python",
"args": ["-m", "aais_tools_mcp"],
"cwd": "/ABS/PATH/TO/Project-Infinity/services/aais-tools-mcp",
"env": {
"AAIS_WORKSPACE_ROOT": "/ABS/PATH/TO/Project-Infinity",
"AAIS_TOOLS_MCP_ALLOW_WRITES": "0"
}
}
}
}
```

Set `AAIS_TOOLS_MCP_ALLOW_WRITES` to `"1"` only when you intentionally allow agent writes. Each write/patch call must still pass `"allow_write": true`.

Restart Cursor after saving. Verify with a `list_dir` or `git_status` tool call.

## Run locally (stdio)

```bash
cd services/aais-tools-mcp
AAIS_WORKSPACE_ROOT=/ABS/PATH/TO/Project-Infinity python -m aais_tools_mcp
```

JSON-RPC line protocol (same as Continuity Ledger MCP): `initialize` → `tools/list` → `tools/call`.

## How Jarvis / AAIS uses it

### Env flags (Jarvis)

| Env | Default | Meaning |
|-----|---------|---------|
| `AAIS_JARVIS_TOOLS_MCP` | off | Set to `1` / `true` / `yes` / `on` so Jarvis prefers **stdio MCP** for operator tools |
| `AAIS_TOOLS_MCP_CMD` | `python3 -m aais_tools_mcp` | Optional spawn command override (shell-split) |
| `AAIS_TOOLS_MCP_TIMEOUT_SEC` | `30` | Stdio handshake / call timeout |
| `AAIS_WORKSPACE_ROOT` | repo root via `src/workspace_root.py` | Sandbox root passed into the MCP child (never `/`) |
| `AAIS_TOOLS_MCP_ALLOW_WRITES` | `0` | Server write gate (still requires `allow_write=true` per call) |

When `AAIS_JARVIS_TOOLS_MCP` is enabled, Jarvis calls `src/aais_tools_mcp_client.py` (`AaisOperatorToolsStdioClient`). Spawn or protocol failures **fail-open** to the in-process adapter so chat never dies (same posture as the `/proc` walk guard).

Selection lives in `src/aais_tools_mcp_adapter.py` → `invoke_aais_operator_tool(...)`. Tool turns that name `read_file`, `write_file`, `apply_patch`, `list_dir`, `search_code`, `run_tests`, `git_status`, or `git_diff` go through `JarvisOperator.handle_tool_request` → that helper (one API for both transports).

1. **Local capability adapter** (default when MCP flag is off):

```python
from src.aais_tools_mcp_adapter import invoke_aais_operator_tool

print(invoke_aais_operator_tool("read_file", {"path": "README.md"}))
```

2. **Stdio client** (when `AAIS_JARVIS_TOOLS_MCP=1`):

```python
from src.aais_tools_mcp_client import invoke_aais_operator_tool_stdio

print(invoke_aais_operator_tool_stdio("git_status", {}))
```

3. Existing Jarvis workspace browsing (`WorkspaceTools` / capability workspace lane) remains the primary **browse/search** path for natural-language chat. Structured coding/tool envelopes use the AAIS operator tool names above (MCP or adapter).

## Tests

```bash
cd services/aais-tools-mcp
python -m pytest -q
```

Coverage includes sandbox denials (traversal, `.env`, secrets) and read/write happy paths under a temp workspace.

## Related

- Continuity Ledger MCP: `services/jarvis-memoryboard/mcp`
- MCP plug bridge (observe/assist): `src/mcp_bridge.py`
- Operator Cursor MCP merge: `src/operator_plugin_bootstrap.py`
18 changes: 18 additions & 0 deletions services/aais-tools-mcp/aais_tools_mcp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""AAIS / Jarvis governed workspace tools MCP.

Mythic: Operator Workshop Tools
Engineering: AaisOperatorToolServer package
"""

from __future__ import annotations

__version__ = "0.1.0"

from aais_tools_mcp.capability_adapter import AaisOperatorToolsCapability
from aais_tools_mcp.tools import AaisOperatorToolCatalog

__all__ = [
"AaisOperatorToolCatalog",
"AaisOperatorToolsCapability",
"__version__",
]
4 changes: 4 additions & 0 deletions services/aais-tools-mcp/aais_tools_mcp/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from aais_tools_mcp.server import main

if __name__ == "__main__":
main()
49 changes: 49 additions & 0 deletions services/aais-tools-mcp/aais_tools_mcp/capability_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Local capability adapter — same tools without an MCP client.

Mythic: Workshop Direct Plug
Engineering: AaisOperatorToolsCapability

Inputs: tool name + args (same as MCP tools/call)
Outputs: structured result dict
Constraints: identical sandbox/write policy as the MCP server
Failure modes: unknown tool / sandbox deny → ok=False with reason_code
"""

from __future__ import annotations

from pathlib import Path
from typing import Any

from aais_tools_mcp.tools import AaisOperatorToolCatalog


class AaisOperatorToolsCapability:
"""Thin local adapter so Jarvis/AAIS can invoke tools without stdio MCP."""

capability_id = "aais_operator_tools"
display_name = "AAIS Operator Tools"

def __init__(self, workspace_root: str | Path | None = None) -> None:
self.catalog = AaisOperatorToolCatalog(workspace_root=workspace_root)

def list_tools(self) -> list[str]:
return self.catalog.list_tool_names()

def invoke(self, tool_name: str, args: dict[str, Any] | None = None) -> dict[str, Any]:
result = self.catalog.call(tool_name, args)
return {
"capability_id": self.capability_id,
"tool": tool_name,
"result": result,
"transport": "local_adapter",
}

def snapshot(self) -> dict[str, Any]:
return {
"capability_id": self.capability_id,
"display_name": self.display_name,
"tools": self.list_tools(),
"writes_env": "AAIS_TOOLS_MCP_ALLOW_WRITES",
"workspace_root": str(self.catalog.sandbox.resolve_root()),
"mcp_server": "services/aais-tools-mcp",
}
55 changes: 55 additions & 0 deletions services/aais-tools-mcp/aais_tools_mcp/evidence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Mutation evidence / audit log for AAIS operator tools.

Mythic: Workshop Trace Ledger
Engineering: MutationEvidenceLog

Inputs: mutation event dict
Outputs: appended JSONL line under .runtime/aais-tools-mcp/
Constraints: never stores secret file contents; gitignored via .runtime/
Failure modes: disk error → event still returned to caller with log_error set
"""

from __future__ import annotations

import json
import os
from datetime import datetime, timezone
from pathlib import Path
from typing import Any


def _utc_now() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")


class MutationEvidenceLog:
"""Append-only JSONL audit log for write/patch/command mutations."""

def __init__(self, workspace_root: Path) -> None:
self.workspace_root = workspace_root
self.log_dir = workspace_root / ".runtime" / "aais-tools-mcp"
self.log_path = self.log_dir / "mutations.jsonl"

def record(self, event: dict[str, Any]) -> dict[str, Any]:
payload = {
"ts": _utc_now(),
"server": "aais-tools-mcp",
**event,
}
try:
self.log_dir.mkdir(parents=True, exist_ok=True)
with self.log_path.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(payload, sort_keys=True) + "\n")
payload["evidence_path"] = str(self.log_path.relative_to(self.workspace_root))
payload["logged"] = True
except OSError as exc:
payload["logged"] = False
payload["log_error"] = str(exc)
return payload


def writes_allowed(*, explicit_allow: bool = False) -> bool:
"""Writes require env policy and/or explicit per-call allow flag."""
env_flag = os.getenv("AAIS_TOOLS_MCP_ALLOW_WRITES", "0").strip().lower()
env_ok = env_flag in {"1", "true", "yes", "on"}
return bool(env_ok and explicit_allow)
Loading
Loading