feat: add AI Workspace context MCP configuration and enhance communication channel filtering in server
This commit is contained in:
8
.agents/mcp_config.json
Normal file
8
.agents/mcp_config.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"aiw-context-mcp": {
|
||||
"url": "http://127.0.0.1:8765/mcp",
|
||||
"serverUrl": "http://127.0.0.1:8765/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,11 @@
|
||||
"OBSIDIAN_HOST": "127.0.0.1",
|
||||
"OBSIDIAN_PORT": "27124"
|
||||
}
|
||||
},
|
||||
"aiw-context-mcp": {
|
||||
"type": "remote",
|
||||
"url": "http://127.0.0.1:8765/mcp",
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"permission": {
|
||||
|
||||
15
profiles/fidelity/context-sources.json
Normal file
15
profiles/fidelity/context-sources.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"profile": "fidelity",
|
||||
"communication_sources": {
|
||||
"mattermost": {
|
||||
"type": "mattermost_mirror",
|
||||
"context_channels": [
|
||||
"fidelity-preguntas",
|
||||
"fidelity-standup",
|
||||
"fidelity-code-review",
|
||||
"fidelity-interface-meetings-on-calendar-outlook-team-etc",
|
||||
"dm-david--jeff"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,8 @@ python3 scripts/mcp/aiw-context-mcp/server.py --transport stdio
|
||||
|
||||
All tools are read-only. Mattermost tools read `ai/inbox/mattermost-mirror/`; photo tools list local Photo Inbox files without embedding image data; project tools read canonical Markdown under `project-knowledge/`.
|
||||
|
||||
Mattermost latest/date/standup tools filter to the active profile's context channels by default. For Fidelity, that list lives in `profiles/fidelity/context-sources.json`. Pass explicit `channels` to override the profile list, or `include_all_channels: true` when broad unfiltered mirror evidence is intentionally needed.
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
|
||||
@@ -70,6 +70,18 @@ def mattermost_mirror_dir(profile: str) -> Path:
|
||||
return inbox_dir(profile) / "mattermost-mirror"
|
||||
|
||||
|
||||
def profile_context_channels(profile: str, source: str = "mattermost") -> set[str]:
|
||||
path = ROOT / "profiles" / profile / "context-sources.json"
|
||||
if not path.is_file():
|
||||
return set()
|
||||
try:
|
||||
config = json.loads(path.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
return set()
|
||||
channels = (((config.get("communication_sources") or {}).get(source) or {}).get("context_channels") or [])
|
||||
return {str(item).strip().lower() for item in channels if str(item).strip()}
|
||||
|
||||
|
||||
def photo_inbox_dir(profile: str) -> Path:
|
||||
configured = os.getenv("PHOTO_INBOX_DIR", "").strip()
|
||||
if configured:
|
||||
@@ -80,10 +92,17 @@ def photo_inbox_dir(profile: str) -> Path:
|
||||
return Path.home() / "Pictures" / "Photo Inbox"
|
||||
|
||||
|
||||
def parse_channels(raw: str | None) -> set[str]:
|
||||
def parse_channels(raw: str | None, profile: str | None = None) -> set[str]:
|
||||
if not raw:
|
||||
raw = os.getenv("AIW_MATTERMOST_CONTEXT_CHANNELS", "") or os.getenv("AIW_MATTERMOST_PROJECT_CHANNELS", "")
|
||||
return {item.strip().lower() for item in (raw or "").split(",") if item.strip()}
|
||||
channels = {item.strip().lower() for item in (raw or "").split(",") if item.strip()}
|
||||
if channels:
|
||||
return channels
|
||||
return profile_context_channels(profile or "fidelity") if profile else set()
|
||||
|
||||
|
||||
def should_use_all_channels(args: dict[str, Any]) -> bool:
|
||||
return bool(args.get("include_all_channels") or args.get("all_channels"))
|
||||
|
||||
|
||||
def previous_workday(today: date) -> date:
|
||||
@@ -171,15 +190,16 @@ def list_profiles(_: dict[str, Any]) -> dict[str, Any]:
|
||||
def communication_latest(args: dict[str, Any]) -> dict[str, Any]:
|
||||
profile = str(args.get("profile") or "fidelity")
|
||||
limit = clamp_limit(args.get("limit"), default=50)
|
||||
records = read_jsonl(mattermost_mirror_dir(profile) / "latest.jsonl", limit=limit)
|
||||
return tool_result({"profile": profile, "source": "mattermost", "evidence_type": "communication", "canonical": False, "records": records})
|
||||
channels = set() if should_use_all_channels(args) else parse_channels(args.get("channels"), profile=profile)
|
||||
records = filter_channels(read_jsonl(mattermost_mirror_dir(profile) / "latest.jsonl", limit=None), channels)[-limit:]
|
||||
return tool_result({"profile": profile, "source": "mattermost", "evidence_type": "communication", "canonical": False, "channel_scope": "all" if not channels else "profile", "channels": sorted(channels), "records": records})
|
||||
|
||||
|
||||
def communication_date_context(args: dict[str, Any]) -> dict[str, Any]:
|
||||
profile = str(args.get("profile") or "fidelity")
|
||||
day = as_date(args.get("date"))
|
||||
limit = clamp_limit(args.get("limit"), default=100)
|
||||
channels = parse_channels(args.get("channels"))
|
||||
channels = set() if should_use_all_channels(args) else parse_channels(args.get("channels"), profile=profile)
|
||||
records = filter_channels(read_jsonl(daily_by_date_path(profile, day), limit=None), channels)[-limit:]
|
||||
return tool_result({"profile": profile, "source": "mattermost", "date": day.isoformat(), "channels": sorted(channels), "records": records, "canonical": False})
|
||||
|
||||
@@ -189,7 +209,7 @@ def communication_standup_context(args: dict[str, Any]) -> dict[str, Any]:
|
||||
today = as_date(args.get("today") or args.get("date"))
|
||||
previous = previous_workday(today)
|
||||
limit = clamp_limit(args.get("limit"), default=80)
|
||||
channels = parse_channels(args.get("channels"))
|
||||
channels = set() if should_use_all_channels(args) else parse_channels(args.get("channels"), profile=profile)
|
||||
previous_records = filter_channels(read_jsonl(daily_by_date_path(profile, previous)), channels)[-limit:]
|
||||
today_records = filter_channels(read_jsonl(daily_by_date_path(profile, today)), channels)[-limit:]
|
||||
return tool_result({
|
||||
@@ -277,9 +297,9 @@ def photos_latest(args: dict[str, Any]) -> dict[str, Any]:
|
||||
|
||||
TOOLS: dict[str, dict[str, Any]] = {
|
||||
"context_profiles": {"handler": list_profiles, "description": "List AI Workspace project profiles.", "properties": {}},
|
||||
"communication_latest": {"handler": communication_latest, "description": "Read bounded latest Mattermost mirror evidence.", "properties": {"profile": {"type": "string"}, "limit": {"type": "integer"}}},
|
||||
"communication_date_context": {"handler": communication_date_context, "description": "Read Mattermost mirror evidence for one date, optionally filtered by channels.", "properties": {"profile": {"type": "string"}, "date": {"type": "string"}, "channels": {"type": "string"}, "limit": {"type": "integer"}}},
|
||||
"communication_standup_context": {"handler": communication_standup_context, "description": "Read previous-workday and today Mattermost evidence for standup drafting.", "properties": {"profile": {"type": "string"}, "today": {"type": "string"}, "channels": {"type": "string"}, "limit": {"type": "integer"}}},
|
||||
"communication_latest": {"handler": communication_latest, "description": "Read bounded latest Mattermost mirror evidence, filtered to the profile's context channels by default.", "properties": {"profile": {"type": "string"}, "channels": {"type": "string"}, "include_all_channels": {"type": "boolean"}, "limit": {"type": "integer"}}},
|
||||
"communication_date_context": {"handler": communication_date_context, "description": "Read Mattermost mirror evidence for one date, filtered to profile context channels by default unless include_all_channels is true.", "properties": {"profile": {"type": "string"}, "date": {"type": "string"}, "channels": {"type": "string"}, "include_all_channels": {"type": "boolean"}, "limit": {"type": "integer"}}},
|
||||
"communication_standup_context": {"handler": communication_standup_context, "description": "Read previous-workday and today Mattermost evidence for standup drafting, filtered to profile context channels by default.", "properties": {"profile": {"type": "string"}, "today": {"type": "string"}, "channels": {"type": "string"}, "include_all_channels": {"type": "boolean"}, "limit": {"type": "integer"}}},
|
||||
"communication_channel_context": {"handler": communication_channel_context, "description": "Read Mattermost mirror evidence for a channel and date.", "properties": {"profile": {"type": "string"}, "channel": {"type": "string"}, "date": {"type": "string"}, "limit": {"type": "integer"}}},
|
||||
"communication_thread_context": {"handler": communication_thread_context, "description": "Read Mattermost mirror evidence for a thread id.", "properties": {"profile": {"type": "string"}, "thread_id": {"type": "string"}, "limit": {"type": "integer"}}},
|
||||
"project_current_context": {"handler": project_current_context, "description": "Read canonical current-work and work-items context.", "properties": {"profile": {"type": "string"}}},
|
||||
|
||||
@@ -61,6 +61,56 @@ class ContextMCPTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual([item["message"] for item in result["records"]], ["m1", "m2"])
|
||||
|
||||
def test_communication_latest_filters_to_profile_channels_by_default(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
latest = root / "ai" / "inbox" / "mattermost-mirror" / "latest.jsonl"
|
||||
profile_config = root / "profiles" / "fidelity" / "context-sources.json"
|
||||
latest.parent.mkdir(parents=True)
|
||||
profile_config.parent.mkdir(parents=True)
|
||||
profile_config.write_text(json.dumps({
|
||||
"communication_sources": {
|
||||
"mattermost": {"context_channels": ["fidelity-code-review", "dm-david--jeff"]}
|
||||
}
|
||||
}), encoding="utf-8")
|
||||
latest.write_text(
|
||||
"\n".join([
|
||||
json.dumps({"post_id": "1", "channel_name": "design-team", "message": "ignore"}),
|
||||
json.dumps({"post_id": "2", "channel_name": "fidelity-code-review", "message": "keep"}),
|
||||
]) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with patch.object(server, "ROOT", root), patch.dict(server.os.environ, {"AIW_MATTERMOST_CONTEXT_CHANNELS": "", "AIW_MATTERMOST_PROJECT_CHANNELS": ""}, clear=False):
|
||||
result = server.communication_latest({"profile": "fidelity", "limit": 10})["structuredContent"]
|
||||
|
||||
self.assertEqual([item["message"] for item in result["records"]], ["keep"])
|
||||
self.assertEqual(result["channel_scope"], "profile")
|
||||
|
||||
def test_communication_latest_can_include_all_channels(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
latest = root / "ai" / "inbox" / "mattermost-mirror" / "latest.jsonl"
|
||||
profile_config = root / "profiles" / "fidelity" / "context-sources.json"
|
||||
latest.parent.mkdir(parents=True)
|
||||
profile_config.parent.mkdir(parents=True)
|
||||
profile_config.write_text(json.dumps({
|
||||
"communication_sources": {"mattermost": {"context_channels": ["fidelity-code-review"]}}
|
||||
}), encoding="utf-8")
|
||||
latest.write_text(
|
||||
"\n".join([
|
||||
json.dumps({"post_id": "1", "channel_name": "design-team", "message": "include"}),
|
||||
json.dumps({"post_id": "2", "channel_name": "fidelity-code-review", "message": "keep"}),
|
||||
]) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with patch.object(server, "ROOT", root), patch.dict(server.os.environ, {"AIW_MATTERMOST_CONTEXT_CHANNELS": "", "AIW_MATTERMOST_PROJECT_CHANNELS": ""}, clear=False):
|
||||
result = server.communication_latest({"profile": "fidelity", "include_all_channels": True, "limit": 10})["structuredContent"]
|
||||
|
||||
self.assertEqual([item["message"] for item in result["records"]], ["include", "keep"])
|
||||
self.assertEqual(result["channel_scope"], "all")
|
||||
|
||||
def test_project_search_skips_templates(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
|
||||
Reference in New Issue
Block a user