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_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": {
|
||||||
|
|||||||
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/`.
|
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
|
||||||
|
|||||||
@@ -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"}}},
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user