feat: add AI Workspace Menu Bar App design and enhance MCP server with resource definitions and read functionality
This commit is contained in:
@@ -48,6 +48,19 @@ All tools are read-only. Mattermost tools read `ai/inbox/mattermost-mirror/`; ph
|
||||
|
||||
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.
|
||||
|
||||
## Resources
|
||||
|
||||
The server also exposes profile resources such as:
|
||||
|
||||
```text
|
||||
aiw://profiles/fidelity/current-work
|
||||
aiw://profiles/fidelity/work-items
|
||||
aiw://profiles/fidelity/mattermost/latest
|
||||
aiw://profiles/fidelity/photos/latest
|
||||
```
|
||||
|
||||
See `client-configs.md` for client setup snippets and verification prompts.
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
|
||||
69
scripts/mcp/aiw-context-mcp/client-configs.md
Normal file
69
scripts/mcp/aiw-context-mcp/client-configs.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# AIW Context MCP Client Config Notes
|
||||
|
||||
The AIW Context MCP server supports both local HTTP and stdio.
|
||||
|
||||
## Preferred HTTP endpoint
|
||||
|
||||
When the Service Manager is running:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:8765/mcp
|
||||
```
|
||||
|
||||
Health check:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:8765/health
|
||||
```
|
||||
|
||||
Use this for clients that support MCP Streamable HTTP/local HTTP servers.
|
||||
|
||||
## stdio command
|
||||
|
||||
Use this for clients that launch MCP servers as subprocesses:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"aiw-context-mcp": {
|
||||
"command": "python3",
|
||||
"args": [
|
||||
"/Users/david/Developer/fidelity-ai-workspace/scripts/mcp/aiw-context-mcp/server.py",
|
||||
"--transport",
|
||||
"stdio"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Antigravity verification prompt
|
||||
|
||||
```text
|
||||
Use the AI Workspace MCP server. List available tools and resources. Then read the resource aiw://profiles/fidelity/current-work and call communication_latest with {"profile":"fidelity","limit":100}. Report the channel_scope and unique returned channel names.
|
||||
```
|
||||
|
||||
## Codex/OpenAI-style verification prompt
|
||||
|
||||
```text
|
||||
Check whether the AIW Context MCP server is available. If available, list its tools/resources, then use project_current_context for profile fidelity. State which MCP calls were used. If unavailable, say so explicitly.
|
||||
```
|
||||
|
||||
## Expected tool names
|
||||
|
||||
- `context_profiles`
|
||||
- `communication_latest`
|
||||
- `communication_date_context`
|
||||
- `communication_standup_context`
|
||||
- `communication_channel_context`
|
||||
- `communication_thread_context`
|
||||
- `project_current_context`
|
||||
- `project_search_memory`
|
||||
- `photos_latest`
|
||||
|
||||
## Expected resource URIs
|
||||
|
||||
- `aiw://profiles/fidelity/current-work`
|
||||
- `aiw://profiles/fidelity/work-items`
|
||||
- `aiw://profiles/fidelity/mattermost/latest`
|
||||
- `aiw://profiles/fidelity/photos/latest`
|
||||
@@ -295,6 +295,75 @@ def photos_latest(args: dict[str, Any]) -> dict[str, Any]:
|
||||
return tool_result({"profile": profile, "source": "photo-inbox", "canonical": False, "photos": photos})
|
||||
|
||||
|
||||
def resource_definitions() -> list[dict[str, Any]]:
|
||||
resources: list[dict[str, Any]] = []
|
||||
for profile in sorted(path.name for path in (ROOT / "profiles").iterdir() if (path / "profile.md").is_file()):
|
||||
resources.extend([
|
||||
{
|
||||
"uri": f"aiw://profiles/{profile}/current-work",
|
||||
"name": f"{profile}-current-work",
|
||||
"title": f"{profile} Current Work",
|
||||
"description": "Canonical current work context.",
|
||||
"mimeType": "text/markdown",
|
||||
"annotations": {"audience": ["assistant"], "priority": 0.95},
|
||||
},
|
||||
{
|
||||
"uri": f"aiw://profiles/{profile}/work-items",
|
||||
"name": f"{profile}-work-items",
|
||||
"title": f"{profile} Active Work Items",
|
||||
"description": "Canonical active work-item summary.",
|
||||
"mimeType": "text/markdown",
|
||||
"annotations": {"audience": ["assistant"], "priority": 0.9},
|
||||
},
|
||||
{
|
||||
"uri": f"aiw://profiles/{profile}/mattermost/latest",
|
||||
"name": f"{profile}-mattermost-latest",
|
||||
"title": f"{profile} Mattermost Latest",
|
||||
"description": "Profile-filtered latest Mattermost mirror evidence as JSON.",
|
||||
"mimeType": "application/json",
|
||||
"annotations": {"audience": ["assistant"], "priority": 0.75},
|
||||
},
|
||||
{
|
||||
"uri": f"aiw://profiles/{profile}/photos/latest",
|
||||
"name": f"{profile}-photos-latest",
|
||||
"title": f"{profile} Photo Inbox Latest",
|
||||
"description": "Latest local Photo Inbox file metadata as JSON; image data is not embedded.",
|
||||
"mimeType": "application/json",
|
||||
"annotations": {"audience": ["assistant"], "priority": 0.45},
|
||||
},
|
||||
])
|
||||
return resources
|
||||
|
||||
|
||||
def read_resource(uri: str) -> dict[str, Any] | None:
|
||||
parsed = urllib.parse.urlparse(uri)
|
||||
if parsed.scheme != "aiw":
|
||||
return None
|
||||
parts = [part for part in parsed.path.split("/") if part]
|
||||
if parsed.netloc != "profiles" or len(parts) < 2:
|
||||
return None
|
||||
profile = parts[0]
|
||||
resource = "/".join(parts[1:])
|
||||
base = knowledge_dir(profile)
|
||||
if resource == "current-work":
|
||||
path = base / "01-current" / "current-work.md"
|
||||
if not path.is_file():
|
||||
return None
|
||||
return {"uri": uri, "mimeType": "text/markdown", "text": path.read_text(encoding="utf-8", errors="replace")}
|
||||
if resource == "work-items":
|
||||
path = base / "01-current" / "work-items.md"
|
||||
if not path.is_file():
|
||||
return None
|
||||
return {"uri": uri, "mimeType": "text/markdown", "text": path.read_text(encoding="utf-8", errors="replace")}
|
||||
if resource == "mattermost/latest":
|
||||
data = communication_latest({"profile": profile, "limit": 80})["structuredContent"]
|
||||
return {"uri": uri, "mimeType": "application/json", "text": json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True)}
|
||||
if resource == "photos/latest":
|
||||
data = photos_latest({"profile": profile, "limit": 30})["structuredContent"]
|
||||
return {"uri": uri, "mimeType": "application/json", "text": json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True)}
|
||||
return None
|
||||
|
||||
|
||||
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, filtered to the profile's context channels by default.", "properties": {"profile": {"type": "string"}, "channels": {"type": "string"}, "include_all_channels": {"type": "boolean"}, "limit": {"type": "integer"}}},
|
||||
@@ -342,7 +411,7 @@ def handle_request(message: dict[str, Any]) -> dict[str, Any] | None:
|
||||
requested = str(params.get("protocolVersion") or PROTOCOL_VERSION)
|
||||
return jsonrpc_response(request_id, {
|
||||
"protocolVersion": requested if requested in {PROTOCOL_VERSION, "2025-03-26"} else PROTOCOL_VERSION,
|
||||
"capabilities": {"tools": {"listChanged": False}},
|
||||
"capabilities": {"tools": {"listChanged": False}, "resources": {"listChanged": False}},
|
||||
"serverInfo": {"name": SERVER_NAME, "title": "AI Workspace Context MCP", "version": SERVER_VERSION},
|
||||
"instructions": "Read-only local AI Workspace context. Evidence is not canonical memory unless promoted by the agent/user.",
|
||||
})
|
||||
@@ -350,6 +419,16 @@ def handle_request(message: dict[str, Any]) -> dict[str, Any] | None:
|
||||
return jsonrpc_response(request_id, {})
|
||||
if method == "tools/list":
|
||||
return jsonrpc_response(request_id, {"tools": tool_definitions()})
|
||||
if method == "resources/list":
|
||||
return jsonrpc_response(request_id, {"resources": resource_definitions()})
|
||||
if method == "resources/read":
|
||||
uri = str(params.get("uri") or "")
|
||||
content = read_resource(uri)
|
||||
if content is None:
|
||||
return jsonrpc_error(request_id, -32002, "Resource not found", {"uri": uri})
|
||||
return jsonrpc_response(request_id, {"contents": [content]})
|
||||
if method == "resources/templates/list":
|
||||
return jsonrpc_response(request_id, {"resourceTemplates": []})
|
||||
if method == "tools/call":
|
||||
name = str(params.get("name") or "")
|
||||
arguments = params.get("arguments") or {}
|
||||
|
||||
@@ -34,6 +34,37 @@ class ContextMCPTests(unittest.TestCase):
|
||||
self.assertIn("project_search_memory", names)
|
||||
self.assertIn("communication_latest", names)
|
||||
|
||||
def test_initialize_response_declares_resources(self) -> None:
|
||||
response = server.handle_request({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": server.PROTOCOL_VERSION}})
|
||||
|
||||
self.assertIn("resources", response["result"]["capabilities"])
|
||||
|
||||
def test_resources_list_includes_current_work(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
profile = root / "profiles" / "fidelity" / "profile.md"
|
||||
profile.parent.mkdir(parents=True)
|
||||
profile.write_text("# Fidelity", encoding="utf-8")
|
||||
with patch.object(server, "ROOT", root):
|
||||
response = server.handle_request({"jsonrpc": "2.0", "id": 1, "method": "resources/list"})
|
||||
|
||||
uris = {item["uri"] for item in response["result"]["resources"]}
|
||||
self.assertIn("aiw://profiles/fidelity/current-work", uris)
|
||||
|
||||
def test_resources_read_current_work(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
profile = root / "profiles" / "fidelity" / "profile.md"
|
||||
current = root / "project-knowledge" / "01-current" / "current-work.md"
|
||||
profile.parent.mkdir(parents=True)
|
||||
current.parent.mkdir(parents=True)
|
||||
profile.write_text("# Fidelity", encoding="utf-8")
|
||||
current.write_text("# Current\nImportant", encoding="utf-8")
|
||||
with patch.object(server, "ROOT", root):
|
||||
response = server.handle_request({"jsonrpc": "2.0", "id": 1, "method": "resources/read", "params": {"uri": "aiw://profiles/fidelity/current-work"}})
|
||||
|
||||
self.assertEqual(response["result"]["contents"][0]["text"], "# Current\nImportant")
|
||||
|
||||
def test_unknown_tool_returns_protocol_error(self) -> None:
|
||||
response = server.handle_request({"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "missing", "arguments": {}}})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user