feat: add AI Workspace context MCP configuration and enhance communication channel filtering in server

This commit is contained in:
2026-05-20 15:16:41 -06:00
parent d3e909d39e
commit cfd61bdee3
6 changed files with 109 additions and 9 deletions

8
.agents/mcp_config.json Normal file
View 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"
}
}
}

View File

@@ -23,6 +23,11 @@
"OBSIDIAN_HOST": "127.0.0.1", "OBSIDIAN_HOST": "127.0.0.1",
"OBSIDIAN_PORT": "27124" "OBSIDIAN_PORT": "27124"
} }
},
"aiw-context-mcp": {
"type": "remote",
"url": "http://127.0.0.1:8765/mcp",
"enabled": true
} }
}, },
"permission": { "permission": {

View 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"
]
}
}
}

View File

@@ -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/`. 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 ## Tests
```bash ```bash

View File

@@ -70,6 +70,18 @@ def mattermost_mirror_dir(profile: str) -> Path:
return inbox_dir(profile) / "mattermost-mirror" 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: def photo_inbox_dir(profile: str) -> Path:
configured = os.getenv("PHOTO_INBOX_DIR", "").strip() configured = os.getenv("PHOTO_INBOX_DIR", "").strip()
if configured: if configured:
@@ -80,10 +92,17 @@ def photo_inbox_dir(profile: str) -> Path:
return Path.home() / "Pictures" / "Photo Inbox" 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: if not raw:
raw = os.getenv("AIW_MATTERMOST_CONTEXT_CHANNELS", "") or os.getenv("AIW_MATTERMOST_PROJECT_CHANNELS", "") 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: 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]: def communication_latest(args: dict[str, Any]) -> dict[str, Any]:
profile = str(args.get("profile") or "fidelity") profile = str(args.get("profile") or "fidelity")
limit = clamp_limit(args.get("limit"), default=50) limit = clamp_limit(args.get("limit"), default=50)
records = read_jsonl(mattermost_mirror_dir(profile) / "latest.jsonl", limit=limit) channels = set() if should_use_all_channels(args) else parse_channels(args.get("channels"), profile=profile)
return tool_result({"profile": profile, "source": "mattermost", "evidence_type": "communication", "canonical": False, "records": records}) 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]: def communication_date_context(args: dict[str, Any]) -> dict[str, Any]:
profile = str(args.get("profile") or "fidelity") profile = str(args.get("profile") or "fidelity")
day = as_date(args.get("date")) day = as_date(args.get("date"))
limit = clamp_limit(args.get("limit"), default=100) 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:] 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}) 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")) today = as_date(args.get("today") or args.get("date"))
previous = previous_workday(today) previous = previous_workday(today)
limit = clamp_limit(args.get("limit"), default=80) 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:] 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:] today_records = filter_channels(read_jsonl(daily_by_date_path(profile, today)), channels)[-limit:]
return tool_result({ return tool_result({
@@ -277,9 +297,9 @@ def photos_latest(args: dict[str, Any]) -> dict[str, Any]:
TOOLS: dict[str, dict[str, Any]] = { TOOLS: dict[str, dict[str, Any]] = {
"context_profiles": {"handler": list_profiles, "description": "List AI Workspace project profiles.", "properties": {}}, "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_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, optionally filtered by channels.", "properties": {"profile": {"type": "string"}, "date": {"type": "string"}, "channels": {"type": "string"}, "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.", "properties": {"profile": {"type": "string"}, "today": {"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, 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_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"}}}, "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"}}}, "project_current_context": {"handler": project_current_context, "description": "Read canonical current-work and work-items context.", "properties": {"profile": {"type": "string"}}},

View File

@@ -61,6 +61,56 @@ class ContextMCPTests(unittest.TestCase):
self.assertEqual([item["message"] for item in result["records"]], ["m1", "m2"]) 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: def test_project_search_skips_templates(self) -> None:
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp) root = Path(tmp)